Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions internal/ogc/features/cql/cql_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ import (
"unicode/utf8"

"github.com/PDOK/gokoala/internal/engine/util"
"github.com/PDOK/gokoala/internal/ogc/features/domain"
"github.com/stretchr/testify/assert"
)

// Test to make sure the parser doesn't crash on invalid input.
// Run with: go test -fuzz=Fuzz -fuzztime=10s -run=^$
func FuzzParseToSQL(f *testing.F) {
queryables := []string{
"floors",
"swimming_pool",
"updated",
"geometry",
"event_time",
queryables := []domain.Field{
{Name: "floors"},
{Name: "swimming_pool"},
{Name: "updated"},
{Name: "geometry", IsPrimaryGeometry: true},
{Name: "event_time"},
// 'created' is not a queryable
}

Expand Down
13 changes: 11 additions & 2 deletions internal/ogc/features/cql/listener_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type CommonListener struct {
namedParams map[string]any

// queryables the list of allowed columns in the datasource that can be queried.
queryables []string
queryables []domain.Field

// srid the filter spatial reference identifier (SRID).
srid domain.SRID
Expand Down Expand Up @@ -65,7 +65,16 @@ RETRY:

func (cl *CommonListener) allowAllQueryables() bool {
log.Println("WARNING: using '*' as queryable, this is not recommended")
return len(cl.queryables) == 1 && cl.queryables[0] == "*"
return len(cl.queryables) == 1 && cl.queryables[0].Name == "*"
}

func (l *GeoPackageListener) isQueryable(name string) bool {
for _, q := range l.queryables {
if q.Name == name || q.IsPrimaryGeometry {
return true
}
}
return false
}

// hasWildcard checks if a pattern contains a SQL wildcard: % or _.
Expand Down
40 changes: 36 additions & 4 deletions internal/ogc/features/cql/listener_geopackage.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cql

import (
"fmt"
"slices"
"strings"

"github.com/PDOK/gokoala/internal/engine/types"
Expand All @@ -29,7 +28,7 @@ type GeoPackageListener struct {
*CommonListener
}

func NewGeoPackageListener(randomizer util.Randomizer, queryables []string, srid domain.SRID) *GeoPackageListener {
func NewGeoPackageListener(randomizer util.Randomizer, queryables []domain.Field, srid domain.SRID) *GeoPackageListener {
return &GeoPackageListener{&CommonListener{
stack: types.NewStack(),
namedParams: make(map[string]any),
Expand Down Expand Up @@ -140,6 +139,23 @@ func (l *GeoPackageListener) ExitIsNullPredicate(ctx *parser.IsNullPredicateCont
func (l *GeoPackageListener) ExitSpatialPredicate(ctx *parser.SpatialPredicateContext) {
right := l.stack.Pop()
left := l.stack.Pop()
if left != fmt.Sprintf("\"%s\"", domain.GeomFieldName) {
l.errorListener.Error(fmt.Sprintf("spatial filtering is only supported on field '%s'", domain.GeomFieldName))
return
}

var geomColumn string
for _, q := range l.queryables {
if q.IsPrimaryGeometry {
geomColumn = q.Name
break
}
}
if geomColumn == "" {
l.errorListener.Error("spatial filtering is not supported for this " +
"collection since there is no geometry field available")
return
}

cqlFunction := strings.ToUpper(ctx.SpatialFunction().GetText())
sqlFunction, ok := spatialFunctions[cqlFunction]
Expand All @@ -148,7 +164,7 @@ func (l *GeoPackageListener) ExitSpatialPredicate(ctx *parser.SpatialPredicateCo
return
}

l.stack.Push(fmt.Sprintf("%s(CastAutomagic(%s), %s)", sqlFunction, left, right))
l.stack.Push(fmt.Sprintf("%s(CastAutomagic(\"%s\"), %s)", sqlFunction, geomColumn, right))
}

// ExitSpatialInstance Spatial instances other than bounding boxes
Expand Down Expand Up @@ -179,9 +195,25 @@ func (l *GeoPackageListener) ExitBbox(ctx *parser.BboxContext) {
return withSymbol
}

if ctx.WestBoundLon() == nil {
l.errorListener.Error("missing west bound coordinate (minx) in bounding box")
return
}
west := toNamedParam(ctx.WestBoundLon().GetText())
if ctx.SouthBoundLat() == nil {
l.errorListener.Error("missing south bound coordinate (miny) in bounding box")
return
}
south := toNamedParam(ctx.SouthBoundLat().GetText())
if ctx.EastBoundLon() == nil {
l.errorListener.Error("missing east bound coordinate (maxx) in bounding box")
return
}
east := toNamedParam(ctx.EastBoundLon().GetText())
if ctx.NorthBoundLat() == nil {
l.errorListener.Error("missing north bound coordinate (maxy) in bounding box")
return
}
north := toNamedParam(ctx.NorthBoundLat().GetText())

l.stack.Push(fmt.Sprintf("BuildMbr(%s, %s, %s, %s, %d)", west, south, east, north, l.srid.GetOrDefault()))
Expand Down Expand Up @@ -278,7 +310,7 @@ func (l *GeoPackageListener) ExitGeometryCollection(ctx *parser.GeometryCollecti
// ExitPropertyName Handle column names
func (l *GeoPackageListener) ExitPropertyName(ctx *parser.PropertyNameContext) {
name := ctx.GetText()
if !l.allowAllQueryables() && !slices.Contains(l.queryables, name) {
if !l.allowAllQueryables() && !l.isQueryable(name) {
err := fmt.Sprintf("property '%s' cannot be used in CQL filter, is not a queryable property", name)
l.errorListener.Error(err)
return
Expand Down
Loading
Loading