diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-05-13 00:39:22 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-05-13 00:40:24 +0200 |
| commit | 2fe6a5a42ccb663211f4f4804b78fff3bd9ebdc0 (patch) | |
| tree | 08fe0bcc17dcd617d08a14847375710b80d86d8d /query_test.go | |
| parent | a86a346ecceb7ad409f116474c1593b201012cf2 (diff) | |
| download | go-oblast-2fe6a5a42ccb663211f4f4804b78fff3bd9ebdc0.tar.gz | |
Insert, Upsert, Update, Delete: do not panic on indirection through nil pointer
Diffstat (limited to 'query_test.go')
| -rw-r--r-- | query_test.go | 73 |
1 files changed, 73 insertions, 0 deletions
diff --git a/query_test.go b/query_test.go index 41b0203..382a463 100644 --- a/query_test.go +++ b/query_test.go @@ -346,3 +346,76 @@ func TestUpsertFailsOnMixedAutoFieldState(t *testing.T) { err := store.Upsert(ctx, db, &brokenRecord) assert.ErrEqual(t, err, `cannot decide whether to INSERT or UPDATE record with idx = 0: some "auto" columns are zero, others are not`) } + +func TestUninitializedTransparentPointerStructs(t *testing.T) { + ctx := t.Context() + md := mock.NewDriver() + db := oblast.Wrap(sql.OpenDB(md)) + + // declare a record type that has a transparent pointer struct containing non-primary-key fields + type timestamps struct { + CreatedAt time.Time `db:"created_at"` + DeletedAt *time.Time `db:"deleted_at"` + } + type nestedRecord struct { + ID int64 `db:"id,auto"` + Name string `db:"name"` + *timestamps + } + nestedRecordStore := oblast.MustNewStore[nestedRecord]( + oblast.SqliteDialect(), + oblast.TableNameIs("nested_records"), + oblast.PrimaryKeyIs("id"), + ) + + // declare another record type that has a primary key field within a transparent pointer struct + type commonFields struct { + ID int64 `db:"id,auto"` + CreatedAt time.Time `db:"created_at"` + DeletedAt *time.Time `db:"deleted_at"` + } + type weirdRecord struct { + *commonFields + Name string `db:"name"` + } + weirdRecordStore := oblast.MustNewStore[weirdRecord]( + oblast.SqliteDialect(), + oblast.TableNameIs("weird_records"), + oblast.PrimaryKeyIs("id"), + ) + + // check detection on INSERT + freshBrokenRecord := nestedRecord{ + Name: "foo", + timestamps: nil, // problem: cannot access `freshBrokenRecord.CreatedAt` or `freshBrokenRecord.DeletedAt` + } + err := nestedRecordStore.Insert(ctx, db, &freshBrokenRecord) + assert.ErrEqual(t, err, `refusing to INSERT record with idx = 0: cannot access all mapped fields because field "timestamps" holds a nil pointer`) + err = nestedRecordStore.Upsert(ctx, db, &freshBrokenRecord) + assert.ErrEqual(t, err, `refusing to INSERT or UPDATE record with idx = 0: cannot access all mapped fields because field "timestamps" holds a nil pointer`) + + // check detection on UPDATE + existingBrokenRecord := nestedRecord{ + ID: 42, + Name: "bar", + timestamps: nil, // same problem as above + } + err = nestedRecordStore.Update(ctx, db, existingBrokenRecord) + assert.ErrEqual(t, err, `refusing to UPDATE record with idx = 0: cannot access all mapped fields because field "timestamps" holds a nil pointer`) + err = nestedRecordStore.Upsert(ctx, db, &freshBrokenRecord) + assert.ErrEqual(t, err, `refusing to INSERT or UPDATE record with idx = 0: cannot access all mapped fields because field "timestamps" holds a nil pointer`) + + // check that detection on DELETE does not care about transparent pointer structs as long as they do not contain PK fields + md.ForQuery(`DELETE FROM "nested_records" WHERE "id" = ?`). + ExpectExecWithArgs(42). + AndReturnRowsAffected(1) + must.Succeed(t, nestedRecordStore.Delete(ctx, db, existingBrokenRecord)) + + // check detection on DELETE where it matters + existingWeirdRecord := weirdRecord{ + commonFields: nil, // problem: cannot access `existingWeirdRecord.ID` + Name: "qux", + } + err = weirdRecordStore.Delete(ctx, db, existingWeirdRecord) + assert.ErrEqual(t, err, `refusing to DELETE record with idx = 0: cannot access all primary key fields because field "commonFields" holds a nil pointer`) +} |
