Fix duplicated operator handling

Looking at this TS change !261 (merged) and comparing it with the Rust version, the Rust version seems to be allowing duplicate operators on the same field.

TS checks for different fields and operators:

// multiple operators on the same field
describe.each`
  field
  ${"created"}
  ${"updated"}
  ${"closed"}
`("field: $field", ({ field }) => {
  test.each`
    operator | operatorDescription
    ${"="}   | ${"equals"}
    ${">"}   | ${"greater than"}
    ${">="}  | ${"greater than or equal to"}
    ${"<"}   | ${"less than"}
    ${"<="}  | ${"less than or equal to"}
  `(
    `${field} $operator 2024-01-01 AND ${field} $operator 2024-01-01`,
    ({ operator, operatorDescription }) => {
      expect(
        compileGraphQL(
          `${field} ${operator} 2024-01-01 AND ${field} ${operator} 2024-01-01`
        )
      ).toBe(
        `Error: Operator ${operatorDescription} (\`${operator}\`) can only be used once with \`${field}\`.`
      );
    }
  );
});

describe.each`
  field
  ${"confidential"}
`("field: $field", ({ field }) => {
  test.each`
    operator | operatorDescription
    ${"="}   | ${"equals"}
  `(
    `${field} $operator true AND ${field} $operator false`,
    ({ operator, operatorDescription }) => {
      expect(
        compileGraphQL(
          `${field} ${operator} true AND ${field} ${operator} false`
        )
      ).toBe(
        `Error: Operator ${operatorDescription} (\`${operator}\`) cansed once with \`${field}\`.`
      );
    }
  );
});

While Rust version only validates duplicate `in` operators, not other operators

fn test_multiple_in_for_same_field() {
    assert_eq!(
        compile_graphql("label IN (\"foo\") AND label IN (\"bar\")"),
        "Error: The is one of (`in`) operator can only be used once with `label`."
    );
}

But if we translate the same test cases (feat. Duo), the test fails:

#[rstest]
#[case::created_equals("created", "=", "equals")]
#[case::created_gt("created", ">", "greater than")]
#[case::created_gte("created", ">=", "greater than or equal to")]
#[case::created_lt("created", "<", "less than")]
#[case::created_lte("created", "<=", "less than or equal to")]
#[case::updated_equals("updated", "=", "equals")]
#[case::updated_gt("updated", ">", "greater than")]
#[case::updated_gte("updated", ">=", "greater than or equal to")]
#[case::updated_lt("updated", "<", "less than")]
#[case::updated_lte("updated", "<=", "less than or equal to")]
#[case::closed_equals("closed", "=", "equals")]
#[case::closed_gt("closed", ">", "greater than")]
#[case::closed_gte("closed", ">=", "greater than or equal to")]
#[case::closed_lt("closed", "<", "less than")]
#[case::closed_lte("closed", "<=", "less than or equal to")]
fn test_duplicate_operator_on_date_field(
    #[case] field: &str,
    #[case] operator: &str,
    #[case] operator_desc: &str,
) {
    let query = format!("{} {} 2024-01-01 AND {} {} 2024-02-01", field, operator, field, operator);
    let expected = format!(
        "Error: Operator {} (`{}`) can only be used once with `{}`.",
        operator_desc, operator, field
    );
    assert_eq!(compile_graphql(&query), expected);
}

#[test]
fn test_duplicate_equals_on_confidential() {
    assert_eq!(
        compile_graphql("confidential = true AND confidential = false"),
        "Error: Operator equals (`=`) can only be used once with `confidential`."
    );
}

#[test]
fn test_valid_different_operators_on_same_field() {
    // This test verifies that DIFFERENT operators on the same field are allowed
    // e.g., label = 'foo' AND label in ('bar') should work
    assert_eq!(
        compile_graphql("label = (\"foo\") AND label in (\"bar\")")
            .lines()
            .nth(1)
            .unwrap(),
        "  issues(labelName: [\"foo\"], or: {labelNames: [\"bar\"]}, before: $before, after: $after, first: $limit) {"
    );
}

failures:
    test_duplicate_equals_on_confidential
    test_duplicate_operator_on_date_field::case_01_created_equals
    test_duplicate_operator_on_date_field::case_02_created_gt
    test_duplicate_operator_on_date_field::case_03_created_gte
    test_duplicate_operator_on_date_field::case_04_created_lt
    test_duplicate_operator_on_date_field::case_05_created_lte
    test_duplicate_operator_on_date_field::case_06_updated_equals
    test_duplicate_operator_on_date_field::case_07_updated_gt
    test_duplicate_operator_on_date_field::case_08_updated_gte
    test_duplicate_operator_on_date_field::case_09_updated_lt
    test_duplicate_operator_on_date_field::case_10_updated_lte
    test_duplicate_operator_on_date_field::case_11_closed_equals
    test_duplicate_operator_on_date_field::case_12_closed_gt
    test_duplicate_operator_on_date_field::case_13_closed_gte
    test_duplicate_operator_on_date_field::case_14_closed_lt
    test_duplicate_operator_on_date_field::case_15_closed_lte
Edited Nov 11, 2025 by Daniele Rossetti
Assignee Loading
Time tracking Loading