Repository: stephenafamo/bob (go)

Issue:
Invalid code generation for PostgreSQL BYTEA 
Hello, I am just exploring bob for the first time.

My database schema has a column of type `BYTEA` and the generated code fails with the following compiler error
`invalid operation: new.Key != old.Key (slice can only be compared to nil)`

It is in the `ReloadAll` method of the slice type of the corresponding table.

I've temporarily replaced the line `if new.Key != old.Key {` with `if !reflect.DeepEqual(new.Key, old.Key) {` and at least get the program to compile.




Discussion:
Thanks for reporting this.

The issue is that `BYTEA` columns are mapped to `[]byte` and I'm guessing you're using this column as your primary key, so it tries to do a comparison to know which model is for which row.

While `reflect.DeepEqual` would work, I wouldn't want to use reflection in such a hot path as it isn't very performant.

Instead, I would rather use [slices.Compare](https://pkg.go.dev/slices#Compare) when the primary key type is a slice.
I just double checked, and you are right the primary key is of type `BYTEA` and the generated code maps this to `[]byte`.
I use other columns with type `BYTEA` in the same table as well, not sure if this is of relevance.

Totally understand the concern about reflect, it was just my first time testing bob and didn't want to abandon my exploration at this early stage.
It would be nice if the code generator handles this, otherwise it gets overwritten each time.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## Added
 
-- Add new properties `cmp_opts` and `cmp_opts_imports` to the `types` configuration. To enable better control over testing randomization.
+- Add new properties `compare_expr` and `compare_expr_imports` to the `types` configuration. This is used when comparing primary keys and in testing.
 
 ## [v0.25.0] - 2024-01-20
 
diff --git a/gen/bobgen-helpers/helpers.go b/gen/bobgen-helpers/helpers.go
--- a/gen/bobgen-helpers/helpers.go
+++ b/gen/bobgen-helpers/helpers.go
@@ -162,17 +162,28 @@ func AddPgEnumArrayType(types drivers.Types, enum string) string {
 }
 
 func AddPgGenericArrayType(types drivers.Types, singleTyp string) string {
+	singleTypDef := types[singleTyp]
+	singleComparer := strings.ReplaceAll(singleTypDef.CompareExpr, "AAA", "a")
+	singleComparer = strings.ReplaceAll(singleComparer, "BBB", "b")
+	if singleComparer == "" {
+		singleComparer = "a == b"
+	}
+
 	typ := fmt.Sprintf("parray.Array[%s]", singleTyp)
-	imports := importers.List{parrayImport}
-	imports = append(imports, types[singleTyp].Imports...)
 
 	types[typ] = drivers.Type{
-		Imports: imports,
+		Imports: append(importers.List{parrayImport}, singleTypDef.Imports...),
 		RandomExpr: fmt.Sprintf(`arr := make(%s, f.IntBetween(1, 5))
             for i := range arr {
                 arr[i] = random[%s](f)
             }
             return any(arr).(T)`, typ, singleTyp),
+		CompareExpr: fmt.Sprintf(`slices.EqualFunc(AAA, BBB, func(a, b %s) bool {
+                return %s
+            })`, singleTyp, singleComparer),
+		CompareExprImports: append(
+			importers.List{`"slices"`},
+			singleTypDef.CompareExprImports...),
 	}
 
 	return typ
@@ -180,12 +191,17 @@ func AddPgGenericArrayType(types drivers.Types, singleTyp string) string {
 
 func Types() drivers.Types {
 	return drivers.Types{
+		"[]byte": {
+			CompareExpr:        `bytes.Equal(AAA, BBB)`,
+			CompareExprImports: importers.List{`"bytes"`},
+		},
 		"time.Time": {
 			Imports: importers.List{`"time"`},
 			RandomExpr: `year := time.Hour * 24 * 365
-                min := time.Now().Add(-1 * year)
+                min := time.Now().Add(-year)
                 max := time.Now().Add(year)
                 return any(f.Time().TimeBetween(min, max)).(T)`,
+			CompareExpr: `AAA.Equal(BBB)`,
 		},
 		"netip.Addr": {
 			Imports: importers.List{`"net/netip"`},
@@ -193,13 +209,13 @@ func Types() drivers.Types {
                 rand.Read(addr[:])
                 return any(netip.AddrFrom4(addr)).(T)`,
 			RandomExprImports: importers.List{`"crypto/rand"`},
-			CmpOptions:        []string{"cmpopts.EquateComparable(netip.Addr{})"},
-			CmpOptionsImports: importers.List{`"github.com/google/go-cmp/cmp/cmpopts"`},
 		},
 		"net.HardwareAddr": {
 			Imports: importers.List{`"net"`},
 			RandomExpr: `addr, _ := net.ParseMAC(f.Internet().MacAddress())
                 return any(addr).(T)`,
+			CompareExpr:        `slices.Equal(AAA, BBB)`,
+			CompareExprImports: importers.List{`"slices"`},
 		},
 		"pq.BoolArray": {
 			Imports: importers.List{`"github.com/lib/pq"`},
@@ -217,6 +233,8 @@ func Types() drivers.Types {
                     arr[i] = f.Int64()
                 }
                 return any(arr).(T)`,
+			CompareExpr:        `slices.Equal(AAA, BBB)`,
+			CompareExprImports: importers.List{`"slices"`},
 		},
 		"pq.ByteaArray": {
 			Imports: importers.List{`"github.com/lib/pq"`},
@@ -225,6 +243,10 @@ func Types() drivers.Types {
                     arr[i] = random[[]byte](f)
                 }
                 return any(arr).(T)`,
+			CompareExpr: `slices.EqualFunc(AAA, BBB, func(a, b []byte) bool {
+                return bytes.Equal(a, b)
+            })`,
+			CompareExprImports: importers.List{`"slices"`, `"bytes"`},
 		},
 		"pq.StringArray": {
 			Imports: importers.List{`"github.com/lib/pq"`},
@@ -233,6 +255,8 @@ func Types() drivers.Types {
                     arr[i] = random[string](f)
                 }
                 return any(arr).(T)`,
+			CompareExpr:        `slices.Equal(AAA, BBB)`,
+			CompareExprImports: importers.List{`"slices"`},
 		},
 		"pq.Float64Array": {
 			Imports: importers.List{`"github.com/lib/pq"`},
@@ -241,6 +265,8 @@ func Types() drivers.Types {
                     arr[i] = f.Float64()
                 }
                 return any(arr).(T)`,
+			CompareExpr:        `slices.Equal(AAA, BBB)`,
+			CompareExprImports: importers.List{`"slices"`},
 		},
 		"pgeo.Box": {
 			Imports:    importers.List{`"github.com/saulortega/pgeo"`},
@@ -259,16 +285,19 @@ func Types() drivers.Types {
 			RandomExpr: `return any(pgeo.NewRandLseg()).(T)`,
 		},
 		"pgeo.Path": {
-			Imports:    importers.List{`"github.com/saulortega/pgeo"`},
-			RandomExpr: `return any(pgeo.NewRandPath()).(T)`,
+			Imports:     importers.List{`"github.com/saulortega/pgeo"`},
+			RandomExpr:  `return any(pgeo.NewRandPath()).(T)`,
+			CompareExpr: `AAA.Closed == BBB.Closed && slices.Equal(AAA.Points, BBB.Points)`,
 		},
 		"pgeo.Point": {
 			Imports:    importers.List{`"github.com/saulortega/pgeo"`},
 			RandomExpr: `return any(pgeo.NewRandPoint()).(T)`,
 		},
 		"pgeo.Polygon": {
-			Imports:    importers.List{`"github.com/saulortega/pgeo"`},
-			RandomExpr: `return any(pgeo.NewRandPolygon()).(T)`,
+			Imports:            importers.List{`"github.com/saulortega/pgeo"`},
+			RandomExpr:         `return any(pgeo.NewRandPolygon()).(T)`,
+			CompareExpr:        `slices.Equal(AAA, BBB)`,
+			CompareExprImports: importers.List{`"slices"`},
 		},
 		"decimal.Decimal": {
 			Imports:    importers.List{`"github.com/shopspring/decimal"`},
@@ -297,7 +326,9 @@ func Types() drivers.Types {
                 }
                 s.WriteRune('}')
                 return any(types.NewJSON[json.RawMessage](s.Bytes())).(T)`,
-			RandomExprImports: importers.List{`"fmt"`, `"bytes"`},
+			RandomExprImports:  importers.List{`"fmt"`, `"bytes"`},
+			CompareExpr:        `bytes.Equal(AAA.Val, BBB.Val)`,
+			CompareExprImports: importers.List{`"bytes"`},
 		},
 	}
 }
diff --git a/gen/drivers/interface.go b/gen/drivers/interface.go
--- a/gen/drivers/interface.go
+++ b/gen/drivers/interface.go
@@ -36,10 +36,12 @@ type Type struct {
 	// Set this to true if the randomization should not be tested
 	// this is useful for low-cardinality types like bool
 	NoRandomizationTest bool `yaml:"no_randomization_test"`
-	// Options to be passed to google.com/go-cmp.Equal in the randomization test
-	CmpOptions []string `yaml:"cmp_options"`
-	// Any options to be imported for the cmp options
-	CmpOptionsImports importers.List `yaml:"cmp_options_imports"`
+	// CompareExpr is used to compare two values of this type
+	// if not provided, == is used
+	// Used AAA and BBB as placeholders for the two values
+	CompareExpr string `yaml:"compare_expr"`
+	// Imports needed for the compare expression
+	CompareExprImports importers.List `yaml:"compare_expr_imports"`
 }
 
 type Types map[string]Type
diff --git a/gen/templates/models/07_slice_methods.go.tpl b/gen/templates/models/07_slice_methods.go.tpl
--- a/gen/templates/models/07_slice_methods.go.tpl
+++ b/gen/templates/models/07_slice_methods.go.tpl
@@ -43,10 +43,17 @@ func (o {{$tAlias.UpSingular}}Slice) ReloadAll(ctx context.Context, exec bob.Exe
 	for _, old := range o {
 		for _, new := range o2 {
 			{{range $column := $table.Constraints.Primary.Columns -}}
-			{{- $colAlias := $tAlias.Column $column -}}
-			if new.{{$colAlias}} != old.{{$colAlias}} {
-				continue
-			}
+				{{- $colAlias := $tAlias.Column $column -}}
+				{{- $typInfo :=  index $.Types ($table.GetColumn $column).Type -}}
+				{{- with $typInfo.CompareExpr -}}
+				  {{- $.Importer.ImportList $typInfo.CompareExprImports -}}
+				  if {{replace "AAA" (cat "new." $colAlias) . | replace "BBB" (cat "old." $colAlias)}}
+				{{- else -}}
+				  if new.{{$colAlias}} != old.{{$colAlias}}
+				{{- end -}}
+				{
+				  continue
+				}
 			{{end -}}
 			{{if $.Relationships.Get $table.Key}}new.R = old.R{{end}}
 			*old = *new
diff --git a/website/docs/code-generation/configuration.md b/website/docs/code-generation/configuration.md
--- a/website/docs/code-generation/configuration.md
+++ b/website/docs/code-generation/configuration.md
@@ -95,41 +95,44 @@ There are things to note about writing the random expression:
 
 To prevent generating a test for the random expression, set `no_randomization_test` to `true`. This is useful for low-cardinality types like `bool`.
 
-When testing the random expression, the value returned is compared to generated previous values using [go-cmp](https://github.com/google/go-cmp/cmp). Sometimes, it is necessary to pass options, and we can do that with the `cmp_options` and `cmp_options_imports` fields.
+In certain cases, it is necessary to compare values of the type to determine if they are equal.
+In such cases, you can provide a `compareExpr` which is an expression that compares two values of the type. Use the placeholders `AAA` and `BBB` as the types to be compared.
 
 ```yaml
 types:
-  pq.BoolArray:
-    # Imports for the type
-    imports: ['"github.com/lib/pq"']
+  type.JSON[json.RawMessage]:
     # If true, a test for the random expression will not be generated
     no_randomization_test: false
-    randomExpr: |-
-      arr := make(pq.BoolArray, f.IntBetween(1, 5))
-      for i := range arr {
-          arr[i] = f.Bool()
-      }
-      return any(arr).(T)
-  netip.Addr:
-    imports: ['"net/netip"']
+    imports:
+      - '"encoding/json"'
+      - '"github.com/stephenafamo/bob/types"'
+    # To be used in factory.random[T]
+    # a variable `f` of type `faker.Faker` is available
+    # since this is in a generic function, the final return should be like
+    # return any(yourVariableOrExpressions).(T)
     random_expr: |-
-      var addr [4]byte
-      rand.Read(addr[:])
-      return any(netip.AddrFrom4(addr)).(T)
-    # ADDITIONAL imports for the random expression
-    random_expr_imports: ['"crypto/rand"']
-    # Pass options to the go-cmp.Equal function
-    cmp_options: ["cmpopts.EquateComparable(netip.Addr{})"]
-    # ADDITIONAL imports for the options
-    cmp_options_imports: ['"github.com/google/go-cmp/cmp/cmpopts"']
-  types.HStore:
-    imports: ['"github.com/stephenafamo/bob/types"']
-    randomExpr: |-
-      hs := make(types.HStore)
+      s := &bytes.Buffer{}
+      s.WriteRune('{')
       for i := 0; i < f.IntBetween(1, 5); i++ {
-          arr[random[string](f)] = randomNull[string](f)
+          if i > 0 {
+              fmt.Fprint(s, ", ")
+          }
+          fmt.Fprintf(s, "%q:%q", f.Lorem().Word(), f.Lorem().Word())
       }
-      return any(hs).(T)
+      s.WriteRune('}')
+      return any(types.NewJSON[json.RawMessage](s.Bytes())).(T)`
+    # Imports for the random expression
+    random_expr_imports:
+      - '"bytes"'
+      - '"fmt"'
+    # CompareExpr is used to compare two values of this type
+    # if not provided, == is used
+    # Used AAA and BBB as placeholders for the two values
+    compare_expr: |-
+      return bytes.Equal(AAA.Val, BBB.Val)
+    # Imports for the compare expression
+    compare_expr_imports:
+      - '"bytes"'
 ```
 
 ## Replacements


Test patch:
diff --git a/gen/templates/factory/singleton/bobfactory_random_test.go.tpl b/gen/templates/factory/singleton/bobfactory_random_test.go.tpl
--- a/gen/templates/factory/singleton/bobfactory_random_test.go.tpl
+++ b/gen/templates/factory/singleton/bobfactory_random_test.go.tpl
@@ -1,5 +1,4 @@
 {{$.Importer.Import "testing"}}
-{{$.Importer.Import "github.com/google/go-cmp/cmp"}}
 
 
 {{$doneTypes := dict }}
@@ -21,9 +20,13 @@
         for i := 0; i < 10; i++ {
           seen[i] = random[{{$colTyp}}](nil)
           for j := 0; j < i; j++ {
-            if cmp.Equal(seen[i], seen[j]
-              {{- with $typInfo.CmpOptions}}{{$.Importer.ImportList $typInfo.CmpOptionsImports}}, {{join ", " .}}{{end -}}
-            ) {
+            {{- with $typInfo.CompareExpr -}}
+              {{- $.Importer.ImportList $typInfo.CompareExprImports -}}
+              if {{replace "AAA" "seen[i]" . | replace "BBB" "seen[j]"}}
+            {{- else -}}
+              if seen[i] == seen[j]
+            {{- end -}}
+            {
               t.Fatalf("random[{{$colTyp}}]() returned the same value twice: %v", seen[i])
             }
           }