quickwit-oss/quickwit · error

both gt and gte are set

Error message

both gt and gte are set

What it means

When converting an Elastic range query into the internal RangeQuery AST, the lower bound is built from `gt` (exclusive) and `gte` (inclusive). Quickwit requires exactly one or neither; supplying both is ambiguous because the two bounds imply different inclusivity, so the conversion bails with this error.

Source

Thrown at quickwit/quickwit-query/src/elastic_query_dsl/range_query.rs:109

        let (gt, gte, lt, lte) = if let Some(JsonLiteral::String(java_date_format)) = format {
            let parser = StrptimeParser::from_java_datetime_format(&java_date_format)
                .map_err(|err| anyhow::anyhow!("failed to parse range query date format. {err}"))?;
            (
                gt.map(|v| parse_and_convert(v, &parser)).transpose()?,
                gte.map(|v| parse_and_convert(v, &parser)).transpose()?,
                lt.map(|v| parse_and_convert(v, &parser)).transpose()?,
                lte.map(|v| parse_and_convert(v, &parser)).transpose()?,
            )
        } else {
            (gt, gte, lt, lte)
        };

        let range_query_ast = crate::query_ast::RangeQuery {
            field,
            lower_bound: match (gt, gte) {
                (Some(_gt), Some(_gte)) => {
                    anyhow::bail!("both gt and gte are set")
                }
                (Some(gt), None) => Bound::Excluded(gt),
                (None, Some(gte)) => Bound::Included(gte),
                (None, None) => Bound::Unbounded,
            },
            upper_bound: match (lt, lte) {
                (Some(_lt), Some(_lte)) => {
                    anyhow::bail!("both lt and lte are set")
                }
                (Some(lt), None) => Bound::Excluded(lt),
                (None, Some(lte)) => Bound::Included(lte),
                (None, None) => Bound::Unbounded,
            },
        };
        let ast: QueryAst = range_query_ast.into();
        Ok(ast.boost(boost))
    }
}

View on GitHub (pinned to a39730c5cd)

Solutions

  1. Remove one of the two keys: keep `gte` for an inclusive lower bound, `gt` for exclusive.
  2. If bounds come from variables, emit only the bound you actually have (skip the other key entirely).
  3. Validate the range request object before submitting to reject duplicated bounds early.

Example fix

// before
{"range": {"timestamp": {"gt": "2024-01-01", "gte": "2024-01-01T00:00:00Z"}}}
// after
{"range": {"timestamp": {"gte": "2024-01-01T00:00:00Z"}}}
Defensive patterns

Strategy: validation

Validate before calling

let keys: Vec<&str> = range.keys().map(|k| k.as_str()).collect();
if keys.contains(&"gt") && keys.contains(&"gte") {
    return Err("range lower bound: set only one of gt/gte");
}

Try / catch

match res {
    Err(e) if e.to_string().contains("both gt and gte") => {
        // drop one bound and retry
    }
    r => r?,
}

Prevention

When it happens

Trigger: Sending a range query JSON containing both `gt` and `gte` keys, e.g. `{"range": {"ts": {"gt": 100, "gte": 50}}}`, through convert_to_query_ast for RangeQuery.

Common situations: Programmatic query builders that unconditionally serialize both bound keys (one null/default); hand-written JSON where the user copied examples mixing gt and gte; template code that fills both boundaries.

Related errors


AI-assisted analysis of quickwit-oss/quickwit@a39730c5cd (2026-09-08). Data as JSON: /api/errors/cf12ec0c436a25aa. Report an issue: GitHub.