Skip to content

CQL2 Filtering

Sanson supports OGC CQL2 for filtering features by attributes and geometry. Both CQL2 Text and CQL2 JSON encodings are supported.

Usage

Add the filter parameter to any features request:

GET /collections/{id}/items?filter=<expression>&filter-lang=cql2-text
GET /collections/{id}/items?filter=<json>&filter-lang=cql2-json

The filter-lang parameter is optional — cql2-text is the default.

CQL2 Text operators

Comparison

OperatorExample
=population = 1000
<>status <> 'closed'
<population < 5000
<=area <= 100.5
>population > 50000
>=elevation >= 1000

Logic

OperatorExample
ANDpopulation > 1000 AND status = 'active'
ORtype = 'city' OR type = 'town'
NOTNOT status = 'closed'

Text

OperatorExample
LIKEname LIKE 'Par%' (case-sensitive)
ILIKEname ILIKE 'par%' (case-insensitive)
NOT LIKEname NOT LIKE 'test%' (negated)
NOT ILIKEname NOT ILIKE '%paris%' (negated)

Use % as a wildcard for zero or more characters.

Null

OperatorExample
IS NULLdescription IS NULL
IS NOT NULLdescription IS NOT NULL

List

OperatorExample
INtype IN ('city', 'town', 'village')
NOT INstatus NOT IN ('closed', 'archived')

Range

OperatorExample
BETWEENpopulation BETWEEN 1000 AND 50000
NOT BETWEENelevation NOT BETWEEN 0 AND 100

BETWEEN works with both numbers and strings.

Spatial

OperatorDescription
S_INTERSECTSGeometry intersects the given geometry
S_WITHINGeometry is within the given geometry
S_CONTAINSGeometry contains the given geometry
S_TOUCHESGeometries touch at their boundaries
S_CROSSESGeometries cross each other
S_OVERLAPSGeometries overlap
S_EQUALSGeometries are spatially equal
S_DISJOINTGeometries are spatially disjoint

Spatial operators use WKT geometry literals:

S_INTERSECTS(geom, POINT(2.35 48.85))
S_WITHIN(geom, POLYGON((2.2 48.8, 2.5 48.8, 2.5 49.0, 2.2 49.0, 2.2 48.8)))
S_CONTAINS(geom, POINT(2.35 48.85))
S_DISJOINT(geom, POLYGON((0 0, 1 0, 1 1, 0 1, 0 0)))

Temporal

OperatorDescription
T_BEFOREProperty is before the given instant
T_AFTERProperty is after the given instant
T_EQUALSProperty equals the given instant
T_DURINGProperty falls within the given interval
T_INTERSECTSProperty intersects the given interval
T_DISJOINTProperty is outside the given interval

Temporal operators use TIMESTAMP(), DATE(), or INTERVAL() literals:

T_BEFORE(updated, TIMESTAMP('2024-01-01T00:00:00Z'))
T_AFTER(created, DATE('2024-06-15'))
T_DURING(updated, INTERVAL('2024-01-01','2024-12-31'))
T_DURING(updated, INTERVAL('2024-01-01','..'))
T_DISJOINT(updated, INTERVAL('..','2024-06-01'))

Open-ended intervals use '..' for unbounded start or end.

CQL2 JSON encoding

CQL2 JSON uses a structured {"op": ..., "args": [...]} format. Properties are referenced as {"property": "name"}.

JSON operators

CQL2 Text equivalentJSON op
=, <>, <, etc.Same (=, <>, <, <=, >, >=)
AND, ORand, or (2+ args)
NOTnot (1 arg)
LIKElike
ILIKElike with casei wrappers
IS NULLisNull
INin (second arg is an array)
BETWEENbetween (3 args)
S_INTERSECTS, etc.s_intersects, etc. (GeoJSON geometry)
T_BEFORE, etc.t_before, etc. (timestamp, date, interval)

JSON examples

Simple comparison:

json
{ "op": "=", "args": [{ "property": "name" }, "Paris"] }

Combined filter:

json
{
  "op": "and",
  "args": [
    { "op": ">", "args": [{ "property": "population" }, 50000] },
    { "op": "=", "args": [{ "property": "departement" }, "75"] }
  ]
}

Spatial filter with GeoJSON:

json
{
  "op": "s_intersects",
  "args": [{ "property": "geom" }, { "type": "Point", "coordinates": [2.35, 48.85] }]
}

Case-insensitive text search:

json
{
  "op": "like",
  "args": [
    { "op": "casei", "args": [{ "property": "nom" }] },
    { "op": "casei", "args": ["saint%"] }
  ]
}

List membership:

json
{ "op": "in", "args": [{ "property": "type" }, ["commune", "arrondissement"]] }

Temporal filter:

json
{
  "op": "t_during",
  "args": [{ "property": "updated" }, { "interval": ["2024-01-01", "2024-12-31"] }]
}

Examples

Simple attribute filter

GET /collections/default:communes/items?filter=population > 100000

Combined filters

GET /collections/default:communes/items
  ?filter=population > 50000 AND departement = '75'

Spatial + attribute filter

GET /collections/default:communes/items
  ?bbox=2.2,48.8,2.5,49.0
  &filter=population > 10000
GET /collections/default:communes/items?filter=nom ILIKE 'saint%'

Full combined query

GET /collections/default:communes/items
  ?bbox=2.0,48.5,3.0,49.5
  &filter=population > 5000 AND type IN ('commune', 'arrondissement')
  &limit=50

Column validation

Column names in CQL2 expressions are validated against the actual table schema. Using an unknown column name returns a 400 Bad Request error.

Use the Queryables endpoint to discover available filterable properties for a collection.

Released under the MIT License.