Repository: xataio/pgstream
Pull request #328 (2025-05-09 11:34:07)

Add neosync lastname transformer
Adding neosync's last name transformer.

github-actions[bot]: ### Merging this branch will **decrease** overall coverage

| Impacted Packages | Coverage Δ | :robot: |
|-------------------|------------|---------|
| github.com/xataio/pgstream/pkg/transformers | 86.76% (ø) |  |
| github.com/xataio/pgstream/pkg/transformers/builder | 20.00% (**-1.05%**) | :thumbsdown: |
| github.com/xataio/pgstream/pkg/transformers/neosync | 87.72% (**-1.06%**) | :thumbsdown: |

---

<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 | 20.00% (**-1.05%**) | 20 (+1) | 4 | 16 (+1) | :thumbsdown: |
| github.com/xataio/pgstream/pkg/transformers/neosync/neosync_lastname_transformer.go | 81.25% (**+81.25%**) | 16 (+16) | 13 (+13) | 3 (+3) | :star2: |
| github.com/xataio/pgstream/pkg/transformers/transformer.go | 96.30% (ø) | 54 | 52 | 2 |  |

_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/neosync/neosync_lastname_transformer_test.go

</details>

github-actions[bot]: ### Merging this branch will **decrease** overall coverage

| Impacted Packages | Coverage Δ | :robot: |
|-------------------|------------|---------|
| github.com/xataio/pgstream/pkg/transformers | 86.76% (ø) |  |
| github.com/xataio/pgstream/pkg/transformers/builder | 20.00% (**-1.05%**) | :thumbsdown: |
| github.com/xataio/pgstream/pkg/transformers/neosync | 87.72% (**-1.06%**) | :thumbsdown: |

---

<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 | 20.00% (**-1.05%**) | 20 (+1) | 4 | 16 (+1) | :thumbsdown: |
| github.com/xataio/pgstream/pkg/transformers/neosync/neosync_lastname_transformer.go | 81.25% (**+81.25%**) | 16 (+16) | 13 (+13) | 3 (+3) | :star2: |
| github.com/xataio/pgstream/pkg/transformers/transformer.go | 96.30% (ø) | 54 | 52 | 2 |  |

_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/neosync/neosync_lastname_transformer_test.go

</details>

github-actions[bot]: ### Merging this branch will **decrease** overall coverage

| Impacted Packages | Coverage Δ | :robot: |
|-------------------|------------|---------|
| github.com/xataio/pgstream/pkg/transformers | 86.76% (ø) |  |
| github.com/xataio/pgstream/pkg/transformers/builder | 20.00% (**-1.05%**) | :thumbsdown: |
| github.com/xataio/pgstream/pkg/transformers/neosync | 87.72% (**-1.06%**) | :thumbsdown: |

---

<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 | 20.00% (**-1.05%**) | 20 (+1) | 4 | 16 (+1) | :thumbsdown: |
| github.com/xataio/pgstream/pkg/transformers/neosync/neosync_lastname_transformer.go | 81.25% (**+81.25%**) | 16 (+16) | 13 (+13) | 3 (+3) | :star2: |
| github.com/xataio/pgstream/pkg/transformers/transformer.go | 96.30% (ø) | 54 | 52 | 2 |  |

_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/neosync/neosync_lastname_transformer_test.go

</details>


Patch:
diff --git a/docs/README.md b/docs/README.md
index 4346a73..e640fdc 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -941,6 +941,36 @@ transformations:
             preserve_length: true
 ```
 
+</details>
+ <details>
+  <summary>neosync_lastname</summary>
+
+**Description:** Generates anonymized last names while optionally preserving length.
+
+| Supported PostgreSQL types          |
+| ----------------------------------- |
+| `text`, `varchar`, `char`, `bpchar` |
+
+| Parameter       | Type | Default | Required |
+| --------------- | ---- | ------- | -------- |
+| preserve_length | bool | false   | No       |
+| max_length      | int  | 100     | No       |
+| seed            | int  | Rand    | No       |
+
+**Example Configuration:**
+
+```yaml
+transformations:
+  table_transformers:
+    - schema: public
+      table: users
+      column_transformers:
+        last_name:
+          name: neosync_lastname
+          parameters:
+            preserve_length: true
+```
+
 </details>
  <details>
   <summary>neosync_string</summary>
diff --git a/pkg/transformers/builder/transformer_builder.go b/pkg/transformers/builder/transformer_builder.go
index f69581a..45589c5 100644
--- a/pkg/transformers/builder/transformer_builder.go
+++ b/pkg/transformers/builder/transformer_builder.go
@@ -40,6 +40,8 @@ func New(cfg *transformers.Config) (transformers.Transformer, error) {
 		return neosync.NewStringTransformer(cfg.Parameters)
 	case transformers.NeosyncFirstName:
 		return neosync.NewFirstNameTransformer(cfg.Parameters)
+	case transformers.NeosyncLastName:
+		return neosync.NewLastNameTransformer(cfg.Parameters)
 	case transformers.NeosyncEmail:
 		return neosync.NewEmailTransformer(cfg.Parameters)
 	case transformers.PhoneNumber:
diff --git a/pkg/transformers/neosync/neosync_lastname_transformer.go b/pkg/transformers/neosync/neosync_lastname_transformer.go
new file mode 100644
index 0000000..ddce4ed
--- /dev/null
+++ b/pkg/transformers/neosync/neosync_lastname_transformer.go
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package neosync
+
+import (
+	"fmt"
+
+	neosynctransformers "github.com/nucleuscloud/neosync/worker/pkg/benthos/transformers"
+	"github.com/xataio/pgstream/pkg/transformers"
+)
+
+type LastNameTransformer struct {
+	*transformer[string]
+}
+
+var lastNameTransformerParams = []string{"preserve_length", "max_length", "seed"}
+
+func NewLastNameTransformer(params transformers.Parameters) (*LastNameTransformer, error) {
+	if err := transformers.ValidateParameters(params, lastNameTransformerParams); err != nil {
+		return nil, err
+	}
+
+	preserveLength, err := findParameter[bool](params, "preserve_length")
+	if err != nil {
+		return nil, fmt.Errorf("neosync_lastname: preserve_length must be a boolean: %w", err)
+	}
+
+	maxLength, err := findParameter[int](params, "max_length")
+	if err != nil {
+		return nil, fmt.Errorf("neosync_lastname: max_length must be an integer: %w", err)
+	}
+
+	seed, err := findParameter[int](params, "seed")
+	if err != nil {
+		return nil, fmt.Errorf("neosync_lastname: seed must be an integer: %w", err)
+	}
+
+	opts, err := neosynctransformers.NewTransformLastNameOpts(toInt64Ptr(maxLength), preserveLength, toInt64Ptr(seed))
+	if err != nil {
+		return nil, err
+	}
+
+	return &LastNameTransformer{
+		transformer: New[string](neosynctransformers.NewTransformLastName(), opts),
+	}, nil
+}
+
+func (t *LastNameTransformer) CompatibleTypes() []transformers.SupportedDataType {
+	return []transformers.SupportedDataType{
+		transformers.StringDataType,
+	}
+}
diff --git a/pkg/transformers/transformer.go b/pkg/transformers/transformer.go
index 0aa68b5..777fb15 100644
--- a/pkg/transformers/transformer.go
+++ b/pkg/transformers/transformer.go
@@ -37,6 +37,7 @@ const (
 	GreenmaskString        TransformerType = "greenmask_string"
 	NeosyncFirstName       TransformerType = "neosync_firstname"
 	GreenmaskFirstName     TransformerType = "greenmask_firstname"
+	NeosyncLastName        TransformerType = "neosync_lastname"
 	NeosyncEmail           TransformerType = "neosync_email"
 	GreenmaskInteger       TransformerType = "greenmask_integer"
 	GreenmaskFloat         TransformerType = "greenmask_float"


Test patch:
diff --git a/pkg/transformers/neosync/neosync_lastname_transformer_test.go b/pkg/transformers/neosync/neosync_lastname_transformer_test.go
new file mode 100644
index 0000000..7144d12
--- /dev/null
+++ b/pkg/transformers/neosync/neosync_lastname_transformer_test.go
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package neosync
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"github.com/xataio/pgstream/pkg/transformers"
+)
+
+func TestNewLastnameTransformer(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name     string
+		params   transformers.Parameters
+		input    any
+		wantErr  error
+		wantName string
+	}{
+		{
+			name: "ok - valid",
+			params: transformers.Parameters{
+				"preserve_length": false,
+				"max_length":      10,
+				"seed":            123,
+			},
+			input:    "lastname",
+			wantErr:  nil,
+			wantName: "Fournaris",
+		},
+		{
+			name: "error - invalid preserve_length",
+			params: transformers.Parameters{
+				"preserve_length": 123,
+				"max_length":      10,
+				"seed":            123,
+			},
+			wantErr: transformers.ErrInvalidParameters,
+		},
+		{
+			name: "error - invalid max_length",
+			params: transformers.Parameters{
+				"preserve_length": true,
+				"max_length":      "invalid",
+				"seed":            123,
+			},
+			wantErr: transformers.ErrInvalidParameters,
+		},
+		{
+			name: "error - invalid seed",
+			params: transformers.Parameters{
+				"preserve_length": true,
+				"max_length":      10,
+				"seed":            "invalid",
+			},
+			wantErr: transformers.ErrInvalidParameters,
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			lst, err := NewLastNameTransformer(tc.params)
+			require.ErrorIs(t, err, tc.wantErr)
+			if tc.wantErr != nil {
+				return
+			}
+			require.NoError(t, err)
+			require.NotNil(t, lst)
+			got, _ := lst.Transform(transformers.Value{TransformValue: tc.input})
+			require.Equal(t, tc.wantName, got)
+		})
+	}
+}