mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-10-28 16:00:24 +01:00
clamped arccosine to [-1,1] to prevent rounding errors in geoDistance
This commit is contained in:
1 parent
dbea21f1b2
commit
df4e6eeb35
4 files changed
+63
-28
No files matched your search
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
- Fixed collection index validator to allow expressions with parenthesis in the optional `WHERE` clause.
|
- Fixed collection index validator to allow expressions with parenthesis in the optional `WHERE` clause.
|
||||||
|
|
||||||
|
- Clamped arccosine to [-1,1] in the Harvesine formula for the `geoDistance()` filter function to workaround edge case related to float rounding errors for some coordinates.
|
||||||
|
|
||||||
- Bumped `golang.org/x/*` dependencies to silence security scanners ([#7829](https://github.com/pocketbase/pocketbase/discussions/7829)).
|
- Bumped `golang.org/x/*` dependencies to silence security scanners ([#7829](https://github.com/pocketbase/pocketbase/discussions/7829)).
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func TestFilterDataBuildExpr(t *testing.T) {
|
|||||||
"geoDistance function",
|
"geoDistance function",
|
||||||
"geoDistance(1,2,3,4) < 567",
|
"geoDistance(1,2,3,4) < 567",
|
||||||
false,
|
false,
|
||||||
"(6371 * acos(cos(radians({:TEST})) * cos(radians({:TEST})) * cos(radians({:TEST}) - radians({:TEST})) + sin(radians({:TEST})) * sin(radians({:TEST})))) < {:TEST}",
|
"(6371 * acos(min(1, max(-1, cos(radians({:TEST})) * cos(radians({:TEST})) * cos(radians({:TEST}) - radians({:TEST})) + sin(radians({:TEST})) * sin(radians({:TEST})))))) < {:TEST}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,14 @@ var TokenFunctions = map[string]func(
|
|||||||
|
|
||||||
return &ResolverResult{
|
return &ResolverResult{
|
||||||
NullFallback: NullFallbackDisabled,
|
NullFallback: NullFallbackDisabled,
|
||||||
Identifier: `(6371 * acos(` +
|
// the clamping is to prevent floating point rounding errors for values like
|
||||||
|
// "1.0002" that could occur for example when comparing identical points
|
||||||
|
// (see the NULL note for arccosine in https://sqlite.org/lang_mathfunc.html#overview)
|
||||||
|
Identifier: `(6371 * acos(min(1, max(-1, ` +
|
||||||
`cos(radians(` + latA + `)) * cos(radians(` + latB + `)) * ` +
|
`cos(radians(` + latA + `)) * cos(radians(` + latB + `)) * ` +
|
||||||
`cos(radians(` + lonB + `) - radians(` + lonA + `)) + ` +
|
`cos(radians(` + lonB + `) - radians(` + lonA + `)) + ` +
|
||||||
`sin(radians(` + latA + `)) * sin(radians(` + latB + `))` +
|
`sin(radians(` + latA + `)) * sin(radians(` + latB + `))` +
|
||||||
`))`,
|
`))))`,
|
||||||
Params: mergeParams(resolvedArgs[0].Params, resolvedArgs[1].Params, resolvedArgs[2].Params, resolvedArgs[3].Params),
|
Params: mergeParams(resolvedArgs[0].Params, resolvedArgs[1].Params, resolvedArgs[2].Params, resolvedArgs[3].Params),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ func TestTokenFunctionsGeoDistance(t *testing.T) {
|
|||||||
baseTokenResolver,
|
baseTokenResolver,
|
||||||
&ResolverResult{
|
&ResolverResult{
|
||||||
NullFallback: NullFallbackDisabled,
|
NullFallback: NullFallbackDisabled,
|
||||||
Identifier: `(6371 * acos(cos(radians({:latA})) * cos(radians({:latB})) * cos(radians({:lonB}) - radians({:lonA})) + sin(radians({:latA})) * sin(radians({:latB}))))`,
|
Identifier: `(6371 * acos(min(1, max(-1, cos(radians({:latA})) * cos(radians({:latB})) * cos(radians({:lonB}) - radians({:lonA})) + sin(radians({:latA})) * sin(radians({:latB}))))))`,
|
||||||
Params: map[string]any{
|
Params: map[string]any{
|
||||||
"lonA": 1,
|
"lonA": 1,
|
||||||
"latA": 2,
|
"latA": 2,
|
||||||
@@ -138,7 +138,7 @@ func TestTokenFunctionsGeoDistance(t *testing.T) {
|
|||||||
baseTokenResolver,
|
baseTokenResolver,
|
||||||
&ResolverResult{
|
&ResolverResult{
|
||||||
NullFallback: NullFallbackDisabled,
|
NullFallback: NullFallbackDisabled,
|
||||||
Identifier: `(6371 * acos(cos(radians({:latA})) * cos(radians({:latB})) * cos(radians({:lonB}) - radians({:lonA})) + sin(radians({:latA})) * sin(radians({:latB}))))`,
|
Identifier: `(6371 * acos(min(1, max(-1, cos(radians({:latA})) * cos(radians({:latB})) * cos(radians({:lonB}) - radians({:lonA})) + sin(radians({:latA})) * sin(radians({:latB}))))))`,
|
||||||
Params: map[string]any{
|
Params: map[string]any{
|
||||||
"lonA": "null",
|
"lonA": "null",
|
||||||
"latA": 2,
|
"latA": 2,
|
||||||
@@ -178,34 +178,64 @@ func TestTokenFunctionsGeoDistanceExec(t *testing.T) {
|
|||||||
t.Error("Expected geoDistance token function to be registered.")
|
t.Error("Expected geoDistance token function to be registered.")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := fn(
|
scenarios := []struct {
|
||||||
func(t fexpr.Token) (*ResolverResult, error) {
|
name string
|
||||||
placeholder := "t" + security.PseudorandomString(5)
|
coords []string // [lonA, latA, lonB, latB]
|
||||||
return &ResolverResult{Identifier: "{:" + placeholder + "}", Params: map[string]any{placeholder: t.Literal}}, nil
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"distinct points",
|
||||||
|
[]string{
|
||||||
|
"23.23033854945808",
|
||||||
|
"42.713146090563384",
|
||||||
|
"23.44920680886216",
|
||||||
|
"42.7078484153991",
|
||||||
|
},
|
||||||
|
"17.89",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identical points (rounding check with 8 lat)",
|
||||||
|
[]string{"0", "8", "0", "8"}, // 8 is an example latitude where rounding happens to result in > 1.0
|
||||||
|
"0.00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identical points (rounding check with 45 lat)",
|
||||||
|
[]string{"0", "45", "0", "45"}, // 45 is an example latitude where rounding happens to result in > 1.0
|
||||||
|
"0.00",
|
||||||
},
|
},
|
||||||
fexpr.Token{Literal: "23.23033854945808", Type: fexpr.TokenNumber},
|
|
||||||
fexpr.Token{Literal: "42.713146090563384", Type: fexpr.TokenNumber},
|
|
||||||
fexpr.Token{Literal: "23.44920680886216", Type: fexpr.TokenNumber},
|
|
||||||
fexpr.Token{Literal: "42.7078484153991", Type: fexpr.TokenNumber},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
column := []float64{}
|
for _, s := range scenarios {
|
||||||
err = testDB.NewQuery("select " + result.Identifier).Bind(result.Params).Column(&column)
|
t.Run(s.name, func(t *testing.T) {
|
||||||
if err != nil {
|
result, err := fn(
|
||||||
t.Fatal(err)
|
func(t fexpr.Token) (*ResolverResult, error) {
|
||||||
}
|
placeholder := "t" + security.PseudorandomString(5)
|
||||||
|
return &ResolverResult{Identifier: "{:" + placeholder + "}", Params: map[string]any{placeholder: t.Literal}}, nil
|
||||||
|
},
|
||||||
|
fexpr.Token{Literal: s.coords[0], Type: fexpr.TokenNumber},
|
||||||
|
fexpr.Token{Literal: s.coords[1], Type: fexpr.TokenNumber},
|
||||||
|
fexpr.Token{Literal: s.coords[2], Type: fexpr.TokenNumber},
|
||||||
|
fexpr.Token{Literal: s.coords[3], Type: fexpr.TokenNumber},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
if len(column) != 1 {
|
column := []float64{}
|
||||||
t.Fatalf("Expected exactly 1 column value as result, got %v", column)
|
err = testDB.NewQuery("select " + result.Identifier).Bind(result.Params).Column(&column)
|
||||||
}
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
expected := "17.89"
|
if len(column) != 1 {
|
||||||
distance := fmt.Sprintf("%.2f", column[0])
|
t.Fatalf("Expected exactly 1 column value as result, got %v", column)
|
||||||
if distance != expected {
|
}
|
||||||
t.Fatalf("Expected distance value %s, got %s", expected, distance)
|
|
||||||
|
distance := fmt.Sprintf("%.2f", column[0])
|
||||||
|
if distance != s.expected {
|
||||||
|
t.Fatalf("Expected distance value %s, got %s", s.expected, distance)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user