aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/assert/assert.go12
-rw-r--r--plan.go11
-rw-r--r--plan_test.go135
-rw-r--r--select_test.go16
4 files changed, 156 insertions, 18 deletions
diff --git a/internal/assert/assert.go b/internal/assert/assert.go
index 99af59c..26f91ff 100644
--- a/internal/assert/assert.go
+++ b/internal/assert/assert.go
@@ -12,7 +12,8 @@ import (
func Equal[V comparable](t testing.TB, actual, expected V) {
t.Helper()
if actual != expected {
- t.Errorf("expected %#v, but got %#v", expected, actual)
+ t.Errorf("expected %#v", expected)
+ t.Errorf(" but got %#v", actual)
}
}
@@ -20,7 +21,8 @@ func Equal[V comparable](t testing.TB, actual, expected V) {
func DeepEqual[V any](t testing.TB, actual, expected V) {
t.Helper()
if !reflect.DeepEqual(actual, expected) {
- t.Errorf("expected %#v, but got %#v", expected, actual)
+ t.Errorf("expected %#v", expected)
+ t.Errorf(" but got %#v", actual)
}
}
@@ -32,7 +34,8 @@ func SliceEqual[V comparable](t testing.TB, actual []V, expected ...V) {
}
for idx := range actual {
if actual[idx] != expected[idx] {
- t.Errorf("element %d: expected %#v, but got %#v", idx, expected[idx], actual[idx])
+ t.Errorf("element %d: expected %#v", idx, expected[idx])
+ t.Errorf("element %d: but got %#v", idx, actual[idx])
}
}
}
@@ -45,7 +48,8 @@ func SliceDeepEqual[V any](t testing.TB, actual []V, expected ...V) {
}
for idx := range actual {
if !reflect.DeepEqual(actual[idx], expected[idx]) {
- t.Errorf("element %d: expected %#v, but got %#v", idx, expected[idx], actual[idx])
+ t.Errorf("element %d: expected %#v", idx, expected[idx])
+ t.Errorf("element %d: but got %#v", idx, actual[idx])
}
}
}
diff --git a/plan.go b/plan.go
index 245deab..8b7ec1d 100644
--- a/plan.go
+++ b/plan.go
@@ -56,7 +56,7 @@ type planOpts struct {
// buildPlan creates a new plan for the given struct type.
func buildPlan(t reflect.Type, dialect Dialect, opts planOpts) (plan, error) {
if t.Kind() != reflect.Struct {
- return plan{}, fmt.Errorf("expected struct type, but got kind %s", t.Kind().String())
+ return plan{}, fmt.Errorf("expected struct type, but got kind %q", t.Kind().String())
}
var p = plan{
@@ -134,7 +134,7 @@ func buildPlan(t reflect.Type, dialect Dialect, opts planOpts) (plan, error) {
case "auto":
p.AutoColumnNames = append(p.AutoColumnNames, columnName)
default:
- return plan{}, fmt.Errorf("unknown tag `db:%q` on field index %v", ","+tag, field.Index)
+ return plan{}, fmt.Errorf("unknown option `db:%q` on field %q", ","+tag, field.Name)
}
}
}
@@ -146,7 +146,7 @@ func buildPlan(t reflect.Type, dialect Dialect, opts planOpts) (plan, error) {
for _, index := range indexesOfUnusedTransparentStructs {
field := t.FieldByIndex(index)
return plan{}, fmt.Errorf(
- "field %q of type %s does not contain any mapped fields (to map this entire field to a DB column, add an explicit `db:\"...\"` tag)",
+ "field %q of type %s does not contain any mapped fields (to map this whole field to a DB column, add an explicit `db:\"...\"` tag)",
field.Name, field.Type.String(),
)
}
@@ -179,9 +179,8 @@ func buildPlan(t reflect.Type, dialect Dialect, opts planOpts) (plan, error) {
p.FillIDWithSetUint = true
default:
return plan{}, fmt.Errorf(
- "column is marked as auto-filled (%s), but this SQL dialect only supports auto-filling struct fields with integer types",
- strings.Join(p.AutoColumnNames, ", "),
- )
+ "column %q is marked as auto-filled, but this SQL dialect only supports auto-filling struct fields with integer types",
+ columnName)
}
default:
return plan{}, fmt.Errorf(
diff --git a/plan_test.go b/plan_test.go
index e9d8dea..772c14a 100644
--- a/plan_test.go
+++ b/plan_test.go
@@ -127,6 +127,74 @@ func TestQueryConstructionBasic(t *testing.T) {
})
}
+func TestQueryConstructionWithOnlyPrimaryKey(t *testing.T) {
+ type relation struct {
+ FooID int64 `db:"foo_id"`
+ BarID int64 `db:"bar_id"`
+ }
+ opts := planOpts{
+ TableName: "foo_bar_relations",
+ PrimaryKeyColumnNames: []string{"foo_id", "bar_id"},
+ }
+
+ t.Run("MysqlDialect", func(t *testing.T) {
+ plan, err := buildPlan(reflect.TypeFor[relation](), MysqlDialect(), opts)
+ if err != nil {
+ t.Error(err)
+ }
+ assert.Equal(t, plan.Select.Query, "SELECT `foo_id`, `bar_id` FROM `foo_bar_relations` WHERE ")
+ assert.DeepEqual(t, plan.Select.ArgumentIndexes, nil)
+ assert.DeepEqual(t, plan.Select.ScanIndexes, [][]int{{0}, {1}})
+ assert.Equal(t, plan.Insert.Query, "INSERT INTO `foo_bar_relations` (`foo_id`, `bar_id`) VALUES (?, ?)")
+ assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{0}, {1}})
+ assert.DeepEqual(t, plan.Insert.ScanIndexes, nil)
+ assert.Equal(t, plan.Update.Query, "")
+ assert.DeepEqual(t, plan.Update.ArgumentIndexes, nil)
+ assert.DeepEqual(t, plan.Update.ScanIndexes, nil)
+ assert.Equal(t, plan.Delete.Query, "DELETE FROM `foo_bar_relations` WHERE `foo_id` = ? AND `bar_id` = ?")
+ assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{0}, {1}})
+ assert.DeepEqual(t, plan.Delete.ScanIndexes, nil)
+ })
+
+ t.Run("PostgresDialect", func(t *testing.T) {
+ plan, err := buildPlan(reflect.TypeFor[relation](), PostgresDialect(), opts)
+ if err != nil {
+ t.Error(err)
+ }
+ assert.Equal(t, plan.Select.Query, `SELECT "foo_id", "bar_id" FROM "foo_bar_relations" WHERE `)
+ assert.DeepEqual(t, plan.Select.ArgumentIndexes, nil)
+ assert.DeepEqual(t, plan.Select.ScanIndexes, [][]int{{0}, {1}})
+ assert.Equal(t, plan.Insert.Query, `INSERT INTO "foo_bar_relations" ("foo_id", "bar_id") VALUES ($1, $2)`)
+ assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{0}, {1}})
+ assert.DeepEqual(t, plan.Insert.ScanIndexes, nil)
+ assert.Equal(t, plan.Update.Query, "")
+ assert.DeepEqual(t, plan.Update.ArgumentIndexes, nil)
+ assert.DeepEqual(t, plan.Update.ScanIndexes, nil)
+ assert.Equal(t, plan.Delete.Query, `DELETE FROM "foo_bar_relations" WHERE "foo_id" = $1 AND "bar_id" = $2`)
+ assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{0}, {1}})
+ assert.DeepEqual(t, plan.Delete.ScanIndexes, nil)
+ })
+
+ t.Run("SqliteDialect", func(t *testing.T) {
+ plan, err := buildPlan(reflect.TypeFor[relation](), SqliteDialect(), opts)
+ if err != nil {
+ t.Error(err)
+ }
+ assert.Equal(t, plan.Select.Query, `SELECT "foo_id", "bar_id" FROM "foo_bar_relations" WHERE `)
+ assert.DeepEqual(t, plan.Select.ArgumentIndexes, nil)
+ assert.DeepEqual(t, plan.Select.ScanIndexes, [][]int{{0}, {1}})
+ assert.Equal(t, plan.Insert.Query, `INSERT INTO "foo_bar_relations" ("foo_id", "bar_id") VALUES (?, ?)`)
+ assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{0}, {1}})
+ assert.DeepEqual(t, plan.Insert.ScanIndexes, nil)
+ assert.Equal(t, plan.Update.Query, "")
+ assert.DeepEqual(t, plan.Update.ArgumentIndexes, nil)
+ assert.DeepEqual(t, plan.Update.ScanIndexes, nil)
+ assert.Equal(t, plan.Delete.Query, `DELETE FROM "foo_bar_relations" WHERE "foo_id" = ? AND "bar_id" = ?`)
+ assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{0}, {1}})
+ assert.DeepEqual(t, plan.Delete.ScanIndexes, nil)
+ })
+}
+
func TestQueryConstructionWithoutPrimaryKey(t *testing.T) {
type relation struct {
FooID int64 `db:"foo_id"`
@@ -337,3 +405,70 @@ func TestQueryConstructionWithMultipleAutoColumns(t *testing.T) {
assert.Equal(t, err.Error(), `cannot use type oblast.record for queries: multiple columns are marked as auto-filled (id, created_at), but this SQL dialect only supports at most one per table`)
})
}
+
+func TestPlanErrorCases(t *testing.T) {
+ type recordUsedViaPointer struct {
+ ID int64 `db:"id"`
+ }
+
+ _, err := NewStore[*recordUsedViaPointer](SqliteDialect())
+ assert.Equal(t, err.Error(), `cannot use type *oblast.recordUsedViaPointer for queries: `+
+ `expected struct type, but got kind "ptr"`)
+
+ type recordWithDuplicateTags struct {
+ Foo int64 `db:"Bar"`
+ Qux float64
+ Bar string
+ }
+ _, err = NewStore[recordWithDuplicateTags](SqliteDialect())
+ assert.Equal(t, err.Error(), `cannot use type oblast.recordWithDuplicateTags for queries: `+
+ "duplicate tag `db:\"Bar\"` on field index [0], but also on field index [2]")
+
+ type recordWithUnusedTransparentStruct struct {
+ ID int64
+ CreatedAt time.Time // has no exported fields!
+ }
+ _, err = NewStore[recordWithUnusedTransparentStruct](SqliteDialect())
+ assert.Equal(t, err.Error(), `cannot use type oblast.recordWithUnusedTransparentStruct for queries: `+
+ "field \"CreatedAt\" of type time.Time does not contain any mapped fields (to map this whole field to a DB column, add an explicit `db:\"...\"` tag)")
+
+ type recordWithPKButNoTableName struct {
+ ID int64 `db:"id"`
+ Name string `db:"name"`
+ }
+ _, err = NewStore[recordWithPKButNoTableName](SqliteDialect(),
+ PrimaryKeyIs("id"),
+ )
+ assert.Equal(t, err.Error(), `cannot use type oblast.recordWithPKButNoTableName for queries: `+
+ `cannot declare a primary key without also providing the TableNameIs option`)
+
+ type recordWithUnknownPK struct {
+ ID int64 `db:"id"`
+ Name string `db:"name"`
+ }
+ _, err = NewStore[recordWithUnknownPK](SqliteDialect(),
+ TableNameIs("records"),
+ PrimaryKeyIs("record_id"),
+ )
+ assert.Equal(t, err.Error(), `cannot use type oblast.recordWithUnknownPK for queries: `+
+ "no field has tag `db:\"record_id\"`, but a field of this name was declared in the primary key")
+
+ type recordWithNonintegerAutoKey struct {
+ CreatedAt time.Time `db:"created_at,auto"`
+ Name string `db:"name"`
+ }
+ _, err = NewStore[recordWithNonintegerAutoKey](SqliteDialect(),
+ TableNameIs("records"),
+ )
+ assert.Equal(t, err.Error(), `cannot use type oblast.recordWithNonintegerAutoKey for queries: `+
+ `column "created_at" is marked as auto-filled, but this SQL dialect only supports auto-filling struct fields with integer types`)
+
+ type recordWithWeirdTagOption struct {
+ ID int64 `db:",auto"`
+ Name string `db:",unique"`
+ Description string
+ }
+ _, err = NewStore[recordWithWeirdTagOption](SqliteDialect())
+ assert.Equal(t, err.Error(), `cannot use type oblast.recordWithWeirdTagOption for queries: `+
+ "unknown option `db:\",unique\"` on field \"Name\"")
+}
diff --git a/select_test.go b/select_test.go
index 9fcecc3..f364e1c 100644
--- a/select_test.go
+++ b/select_test.go
@@ -22,11 +22,11 @@ func TestSelectReturningSomeRecords(t *testing.T) {
ID int64 `db:"id"`
Name string `db:"name"`
}
- store := must.Return(oblast.NewStore[basicRecord](
+ store := oblast.MustNewStore[basicRecord](
oblast.SqliteDialect(),
oblast.TableNameIs("basic_records"),
oblast.PrimaryKeyIs("id"),
- ))(t)
+ )
t.Run("using Store.Select", func(t *testing.T) {
md.ForQuery(`SELECT * FROM basic_records WHERE id < ?`).
@@ -83,11 +83,11 @@ func TestSelectReturningNoRecords(t *testing.T) {
ID int64 `db:"id"`
Name string `db:"name"`
}
- store := must.Return(oblast.NewStore[basicRecord](
+ store := oblast.MustNewStore[basicRecord](
oblast.SqliteDialect(),
oblast.TableNameIs("basic_records"),
oblast.PrimaryKeyIs("id"),
- ))(t)
+ )
t.Run("using Store.Select", func(t *testing.T) {
md.ForQuery(`SELECT * FROM basic_records WHERE id < ?`).
@@ -130,11 +130,11 @@ func TestSelectIntoUnexpectedField(t *testing.T) {
ID int64 `db:"id"`
Description string `db:"desc"` // but DB knows only the field "name"!
}
- store := must.Return(oblast.NewStore[basicRecord](
+ store := oblast.MustNewStore[basicRecord](
oblast.SqliteDialect(),
oblast.TableNameIs("basic_records"),
oblast.PrimaryKeyIs("id"),
- ))(t)
+ )
expectedError := "result has column \"name\" in position 0, but no field in type basicRecord has `db:\"name\"`"
@@ -169,11 +169,11 @@ func TestSelectWithScanError(t *testing.T) {
ID int64 `db:"id"`
CreatedAt time.Time `db:"created_at"` // but the DB will give us strings that are not timestamps
}
- store := must.Return(oblast.NewStore[basicRecord](
+ store := oblast.MustNewStore[basicRecord](
oblast.SqliteDialect(),
oblast.TableNameIs("basic_records"),
oblast.PrimaryKeyIs("id"),
- ))(t)
+ )
expectedError := `sql: Scan error on column index 1, name "created_at": unsupported Scan, storing driver.Value type string into type *time.Time`