diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d9e36b5..e81ff5db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- `CountDistinct` finisher to count the distinct values of a column (`COUNT(DISTINCT column)`). + ## [4.19.7] - 2025-12-31 ### Fixed diff --git a/README.md b/README.md index 90054f37..b7e1fcdc 100644 --- a/README.md +++ b/README.md @@ -1356,6 +1356,7 @@ models.Pilots().All(ctx, db) One() // Retrieve one row as object (same as LIMIT(1)) All() // Retrieve all rows as objects (same as SELECT * FROM) Count() // Number of rows (same as COUNT(*)) +CountDistinct("name") // Number of distinct values in a column (same as COUNT(DISTINCT name)) UpdateAll(models.M{"name": "John", "age": 23}) // Update all rows matching the built query. DeleteAll() // Delete all rows matching the built query. Exists() // Returns a bool indicating whether the row(s) for the built query exists. diff --git a/templates/main/03_finishers.go.tpl b/templates/main/03_finishers.go.tpl index 988d5e3d..e4ef02d2 100644 --- a/templates/main/03_finishers.go.tpl +++ b/templates/main/03_finishers.go.tpl @@ -168,6 +168,60 @@ func (q {{$alias.DownSingular}}Query) Count({{if .NoContext}}exec boil.Executor{ return count, nil } +{{if .AddGlobal -}} +// CountDistinctG returns the count of distinct values of the given column in the query using the global executor. +func (q {{$alias.DownSingular}}Query) CountDistinctG({{if not .NoContext}}ctx context.Context, {{end}}column string) (int64, error) { + return q.CountDistinct({{if .NoContext}}boil.GetDB(){{else}}ctx, boil.GetContextDB(){{end}}, column) +} + +{{end -}} + +{{if and .AddGlobal .AddPanic -}} +// CountDistinctGP returns the count of distinct values of the given column in the query using the global executor, and panics on error. +func (q {{$alias.DownSingular}}Query) CountDistinctGP({{if not .NoContext}}ctx context.Context, {{end}}column string) int64 { + c, err := q.CountDistinct({{if .NoContext}}boil.GetDB(){{else}}ctx, boil.GetContextDB(){{end}}, column) + if err != nil { + panic(boil.WrapErr(err)) + } + + return c +} + +{{end -}} + +{{if .AddPanic -}} +// CountDistinctP returns the count of distinct values of the given column in the query, and panics on error. +func (q {{$alias.DownSingular}}Query) CountDistinctP({{if .NoContext}}exec boil.Executor{{else}}ctx context.Context, exec boil.ContextExecutor{{end}}, column string) int64 { + c, err := q.CountDistinct({{if not .NoContext}}ctx, {{end -}} exec, column) + if err != nil { + panic(boil.WrapErr(err)) + } + + return c +} + +{{end -}} + +// CountDistinct returns the count of distinct values of the given column in the query. +func (q {{$alias.DownSingular}}Query) CountDistinct({{if .NoContext}}exec boil.Executor{{else}}ctx context.Context, exec boil.ContextExecutor{{end}}, column string) (int64, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + queries.SetDistinct(q.Query, column) + + {{if .NoContext -}} + err := q.Query.QueryRow(exec).Scan(&count) + {{else -}} + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + {{end -}} + if err != nil { + return 0, errors.Wrap(err, "{{.PkgName}}: failed to count distinct {{.Table.Name}} rows") + } + + return count, nil +} + {{if .AddGlobal -}} // ExistsG checks if the row exists in the table using the global executor. func (q {{$alias.DownSingular}}Query) ExistsG({{if not .NoContext}}ctx context.Context{{end}}) (bool, error) { diff --git a/templates/test/finishers.go.tpl b/templates/test/finishers.go.tpl index c7044353..d7fba1d5 100644 --- a/templates/test/finishers.go.tpl +++ b/templates/test/finishers.go.tpl @@ -112,3 +112,38 @@ func test{{$alias.UpPlural}}Count(t *testing.T) { t.Error("want 2 records, got:", count) } } + +func test{{$alias.UpPlural}}CountDistinct(t *testing.T) { + t.Parallel() + + var err error + seed := randomize.NewSeed() + {{$alias.DownSingular}}One := &{{$alias.UpSingular}}{} + {{$alias.DownSingular}}Two := &{{$alias.UpSingular}}{} + if err = randomize.Struct(seed, {{$alias.DownSingular}}One, {{$alias.DownSingular}}DBTypes, false, {{$alias.DownSingular}}ColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize {{$alias.UpSingular}} struct: %s", err) + } + if err = randomize.Struct(seed, {{$alias.DownSingular}}Two, {{$alias.DownSingular}}DBTypes, false, {{$alias.DownSingular}}ColumnsWithDefault...); err != nil { + t.Errorf("Unable to randomize {{$alias.UpSingular}} struct: %s", err) + } + + {{if not .NoContext}}ctx := context.Background(){{end}} + tx := MustTx({{if .NoContext}}boil.Begin(){{else}}boil.BeginTx(ctx, nil){{end}}) + defer func() { _ = tx.Rollback() }() + if err = {{$alias.DownSingular}}One.Insert({{if not .NoContext}}ctx, {{end -}} tx, boil.Infer()); err != nil { + t.Error(err) + } + if err = {{$alias.DownSingular}}Two.Insert({{if not .NoContext}}ctx, {{end -}} tx, boil.Infer()); err != nil { + t.Error(err) + } + + // Two rows were inserted, so a distinct count of any single column is between 1 and 2. + count, err := {{$alias.UpPlural}}().CountDistinct({{if not .NoContext}}ctx, {{end -}} tx, "{{ (index $.Table.Columns 0).Name | $.Quotes }}") + if err != nil { + t.Error(err) + } + + if count < 1 || count > 2 { + t.Error("want a distinct count between 1 and 2, got:", count) + } +} diff --git a/templates/test/singleton/boil_suites_test.go.tpl b/templates/test/singleton/boil_suites_test.go.tpl index f60abe76..617cf651 100644 --- a/templates/test/singleton/boil_suites_test.go.tpl +++ b/templates/test/singleton/boil_suites_test.go.tpl @@ -142,6 +142,16 @@ func TestCount(t *testing.T) { {{- end -}} } +func TestCountDistinct(t *testing.T) { + {{- range .Tables}} + {{- if or .IsJoinTable .IsView -}} + {{- else -}} + {{- $alias := $.Aliases.Table .Name -}} + t.Run("{{$alias.UpPlural}}", test{{$alias.UpPlural}}CountDistinct) + {{end -}} + {{- end -}} +} + {{if not .NoHooks -}} func TestHooks(t *testing.T) { {{- range .Tables}}