From eb28f898d05de2ef51b3d1808c1d661b77a44987 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Sun, 22 Feb 2026 11:06:08 +0200 Subject: [PATCH] [#7538] set OnlyInt:true when a view column expression is loosely known to return int-only values --- CHANGELOG.md | 5 +++++ core/view.go | 23 +++++++++++++++++--- core/view_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee51aa3a..babfa6a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.36.6 (WIP) + +- Set `NumberField.OnlyInt:true` for the generated View collection schema fields when a view column expression is known to return int-only values ([#7538](https://github.com/pocketbase/pocketbase/issues/7538)). + + ## v0.36.5 - Disabled collection and fields name normalization while in IME mode ([#7532](https://github.com/pocketbase/pocketbase/pull/7532); thanks @miaopan607). diff --git a/core/view.go b/core/view.go index 0f57a1f5..fdfefdf2 100644 --- a/core/view.go +++ b/core/view.go @@ -257,7 +257,16 @@ func parseQueryToFields(app App, selectQuery string) (map[string]*queryField, er } // numeric aggregations - if strings.HasPrefix(colLower, "count(") || strings.HasPrefix(colLower, "total(") { + if strings.HasPrefix(colLower, "count(") { + result[col.alias] = &queryField{ + field: &NumberField{ + Name: col.alias, + OnlyInt: true, + }, + } + continue + } + if strings.HasPrefix(colLower, "total(") { result[col.alias] = &queryField{ field: &NumberField{ Name: col.alias, @@ -268,16 +277,24 @@ func parseQueryToFields(app App, selectQuery string) (map[string]*queryField, er castMatch := castRegex.FindStringSubmatch(colLower) - // numeric casts + // casts if len(castMatch) == 2 { switch castMatch[1] { - case "real", "integer", "int", "decimal", "numeric": + case "real", "decimal", "numeric": result[col.alias] = &queryField{ field: &NumberField{ Name: col.alias, }, } continue + case "int", "integer": + result[col.alias] = &queryField{ + field: &NumberField{ + Name: col.alias, + OnlyInt: true, + }, + } + continue case "text": result[col.alias] = &queryField{ field: &TextField{ diff --git a/core/view_test.go b/core/view_test.go index 3e0a343e..189e1a54 100644 --- a/core/view_test.go +++ b/core/view_test.go @@ -545,6 +545,61 @@ func TestCreateViewFields(t *testing.T) { ensureNoTempViews(app, t) } +func TestCreateViewFieldsWithNumberOnlyInt(t *testing.T) { + t.Parallel() + + app, _ := tests.NewTestApp() + defer app.Cleanup() + + sql := `select + a.id, + count(a.id) count, + total(a.id) total, + cast(a.id as int) cast_int, + cast(a.id as integer) cast_integer, + cast(a.id as real) cast_real, + cast(a.id as decimal) cast_decimal, + cast(a.id as numeric) cast_numeric + from demo1 a` + + result, err := app.CreateViewFields(sql) + if err != nil { + t.Fatal(err) + } + + onlyInts := map[string]bool{ + "count": true, + "total": false, + "cast_int": true, + "cast_integer": true, + "cast_real": false, + "cast_decimal": false, + "cast_numeric": false, + } + + totalExpected := len(onlyInts) + 1 + if total := len(result); total != totalExpected { + t.Fatalf("Expected %d, got %d", totalExpected, total) + } + + for _, f := range result { + if f.GetName() == "id" { + continue + } + + t.Run(f.GetName(), func(t *testing.T) { + nf, ok := f.(*core.NumberField) + if !ok { + t.Fatalf("Expected *core.NumberField, got %v", f) + } + + if nf.OnlyInt != onlyInts[nf.Name] { + t.Fatalf("Expected OnlyInt %v, got %v", onlyInts[nf.Name], nf.OnlyInt) + } + }) + } +} + func TestFindRecordByViewFile(t *testing.T) { t.Parallel()