alamb commented on code in PR #19050:
URL: https://github.com/apache/datafusion/pull/19050#discussion_r2602363180
##########
datafusion/physical-expr/src/expressions/in_list.rs:
##########
@@ -226,31 +226,62 @@ impl StaticFilter for Int32StaticFilter {
}
fn contains(&self, v: &dyn Array, negated: bool) -> Result<BooleanArray> {
+ // Handle dictionary arrays by recursing on the values
+ downcast_dictionary_array! {
+ v => {
+ let values_contains = self.contains(v.values().as_ref(),
negated)?;
+ let result = take(&values_contains, v.keys(), None)?;
+ return Ok(downcast_array(result.as_ref()))
+ }
+ _ => {}
+ }
+
let v = v
.as_primitive_opt::<Int32Type>()
.ok_or_else(|| exec_datafusion_err!("Failed to downcast array"))?;
- let result = match (v.null_count() > 0, negated) {
+ let haystack_has_nulls = self.null_count > 0;
+ let has_nulls = v.null_count() > 0 || haystack_has_nulls;
+
+ let result = match (has_nulls, negated) {
(true, false) => {
- // has nulls, not negated"
- BooleanArray::from_iter(
- v.iter().map(|value| Some(self.values.contains(&value?))),
- )
+ // Either needle or haystack has nulls, not negated
+ BooleanArray::from_iter(v.iter().map(|value| match value {
+ None => None,
+ Some(v) => {
+ if self.values.contains(&v) {
+ Some(true)
+ } else if haystack_has_nulls {
+ None
+ } else {
+ Some(false)
+ }
+ }
+ }))
}
(true, true) => {
- // has nulls, negated
- BooleanArray::from_iter(
- v.iter().map(|value| Some(!self.values.contains(&value?))),
- )
+ // Either needle or haystack has nulls, negated
Review Comment:
```suggestion
// needle has nulls, negated
```
##########
datafusion/physical-expr/src/expressions/in_list.rs:
##########
@@ -2815,4 +2577,321 @@ mod tests {
Ok(())
}
+
+ #[test]
+ fn test_in_list_dictionary_int32() -> Result<()> {
+ // Create schema with dictionary-encoded Int32 column
+ let dict_type =
+ DataType::Dictionary(Box::new(DataType::Int8),
Box::new(DataType::Int32));
+ let schema = Schema::new(vec![Field::new("a", dict_type.clone(),
false)]);
+ let col_a = col("a", &schema)?;
+
+ // Create IN list with Int32 literals: (1, 2, 3)
+ let list = vec![lit(1i32), lit(2i32), lit(3i32)];
+
+ // Create InListExpr via in_list() - this uses Int32StaticFilter for
Int32 lists
+ let expr = in_list(col_a, list, &false, &schema)?;
+
+ // Create dictionary-encoded batch with values [1, 2, 5]
+ // Dictionary: keys [0, 1, 2] -> values [1, 2, 5]
+ let keys = Int8Array::from(vec![0, 1, 2]);
+ let values = Int32Array::from(vec![1, 2, 5]);
Review Comment:
I personally recommend using values that are clearly not the keys - for
example
```suggestion
let values = Int32Array::from(vec![100, 200, 500]);
```
( you would also have to change the literals above)
##########
datafusion/physical-expr/src/expressions/in_list.rs:
##########
@@ -226,31 +226,62 @@ impl StaticFilter for Int32StaticFilter {
}
fn contains(&self, v: &dyn Array, negated: bool) -> Result<BooleanArray> {
+ // Handle dictionary arrays by recursing on the values
+ downcast_dictionary_array! {
+ v => {
+ let values_contains = self.contains(v.values().as_ref(),
negated)?;
+ let result = take(&values_contains, v.keys(), None)?;
+ return Ok(downcast_array(result.as_ref()))
+ }
+ _ => {}
+ }
+
let v = v
.as_primitive_opt::<Int32Type>()
.ok_or_else(|| exec_datafusion_err!("Failed to downcast array"))?;
- let result = match (v.null_count() > 0, negated) {
+ let haystack_has_nulls = self.null_count > 0;
+ let has_nulls = v.null_count() > 0 || haystack_has_nulls;
+
+ let result = match (has_nulls, negated) {
(true, false) => {
- // has nulls, not negated"
- BooleanArray::from_iter(
- v.iter().map(|value| Some(self.values.contains(&value?))),
- )
+ // Either needle or haystack has nulls, not negated
+ BooleanArray::from_iter(v.iter().map(|value| match value {
+ None => None,
+ Some(v) => {
+ if self.values.contains(&v) {
+ Some(true)
+ } else if haystack_has_nulls {
+ None
+ } else {
+ Some(false)
+ }
+ }
+ }))
}
(true, true) => {
- // has nulls, negated
- BooleanArray::from_iter(
- v.iter().map(|value| Some(!self.values.contains(&value?))),
- )
+ // Either needle or haystack has nulls, negated
+ BooleanArray::from_iter(v.iter().map(|value| match value {
+ None => None,
+ Some(v) => {
+ if self.values.contains(&v) {
+ Some(false)
+ } else if haystack_has_nulls {
+ None
+ } else {
+ Some(true)
+ }
+ }
+ }))
}
(false, false) => {
- //no null, not negated
+ // No nulls anywhere, not negated
BooleanArray::from_iter(
v.values().iter().map(|value| self.values.contains(value)),
)
}
(false, true) => {
- // no null, negated
+ // No nulls anywhere, negated
Review Comment:
according to code coverage, this branch is not covered by tests (see PR
comments)
##########
datafusion/physical-expr/src/expressions/in_list.rs:
##########
@@ -226,31 +226,61 @@ impl StaticFilter for Int32StaticFilter {
}
fn contains(&self, v: &dyn Array, negated: bool) -> Result<BooleanArray> {
+ // Handle dictionary arrays by recursing on the values
+ downcast_dictionary_array! {
+ v => {
+ let values_contains = self.contains(v.values().as_ref(),
negated)?;
+ let result = take(&values_contains, v.keys(), None)?;
+ return Ok(downcast_array(result.as_ref()))
+ }
+ _ => {}
+ }
+
let v = v
.as_primitive_opt::<Int32Type>()
.ok_or_else(|| exec_datafusion_err!("Failed to downcast array"))?;
- let result = match (v.null_count() > 0, negated) {
- (true, false) => {
- // has nulls, not negated"
- BooleanArray::from_iter(
- v.iter().map(|value| Some(self.values.contains(&value?))),
Review Comment:
i double checked with postgres and the new code looks right
```sql
postgres=# select NULL IN (1);
?column?
----------
(1 row)
postgres=# select 1 IN (1, NULL);
?column?
----------
t
(1 row)
postgres=# select 1 IN (2, NULL);
?column?
----------
(1 row)
postgres=# select 1 NOT IN (2, NULL);
?column?
----------
(1 row)
```
##########
datafusion/physical-expr/src/expressions/in_list.rs:
##########
@@ -355,6 +386,57 @@ impl InListExpr {
Some(instantiate_static_filter(array)?),
))
}
+
+ /// Create a new InList expression with a static filter for constant list
expressions.
+ ///
+ /// This validates data types, evaluates the list as constants, and uses
specialized
+ /// StaticFilter implementations for better performance (e.g.,
Int32StaticFilter for Int32).
+ ///
+ /// Returns an error if data types don't match or if the list contains
non-constant expressions.
+ pub fn try_from_static_filter(
+ expr: Arc<dyn PhysicalExpr>,
+ list: Vec<Arc<dyn PhysicalExpr>>,
+ negated: bool,
+ schema: &Schema,
+ ) -> Result<Self> {
+ // Check the data types match
+ let expr_data_type = expr.data_type(schema)?;
+ for list_expr in list.iter() {
+ let list_expr_data_type = list_expr.data_type(schema)?;
+ assert_or_internal_err!(
+ DFSchema::datatype_is_logically_equal(
+ &expr_data_type,
+ &list_expr_data_type
+ ),
+ "The data type inlist should be same, the value type is
{expr_data_type}, one of list expr type is {list_expr_data_type}"
+ );
+ }
+
+ match try_evaluate_constant_list(&list, schema) {
+ Ok(in_array) => Ok(Self::new(
+ expr,
+ list,
+ negated,
+ Some(instantiate_static_filter(in_array)?),
+ )),
+ Err(_) => {
Review Comment:
nit -- I think it would be clearer if this `try_evaluate_constant_list`
evaluated to Result<Option<..>> rather than Result -- that way we avoid the
string allocation on error, and could pass real errors .
This code responds to all errors, even if the issue is something different
than the types are not supported
If you went this route, I suspect you wouldn't need the second error check
for type matches (it would already be handled)
##########
datafusion/physical-expr/src/expressions/in_list.rs:
##########
@@ -698,562 +750,370 @@ mod tests {
}};
}
- #[test]
- fn in_list_utf8() -> Result<()> {
- let schema = Schema::new(vec![Field::new("a", DataType::Utf8, true)]);
- let a = StringArray::from(vec![Some("a"), Some("d"), None]);
- let col_a = col("a", &schema)?;
- let batch = RecordBatch::try_new(Arc::new(schema.clone()),
vec![Arc::new(a)])?;
-
- // expression: "a in ("a", "b")"
- let list = vec![lit("a"), lit("b")];
- in_list!(
- batch,
- list,
- &false,
- vec![Some(true), Some(false), None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a not in ("a", "b")"
- let list = vec![lit("a"), lit("b")];
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), Some(true), None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a in ("a", "b", null)"
- let list = vec![lit("a"), lit("b"), lit(ScalarValue::Utf8(None))];
- in_list!(
- batch,
- list,
- &false,
- vec![Some(true), None, None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a not in ("a", "b", null)"
- let list = vec![lit("a"), lit("b"), lit(ScalarValue::Utf8(None))];
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), None, None],
- Arc::clone(&col_a),
- &schema
- );
-
- Ok(())
+ /// Test case for primitive types following the standard IN LIST pattern.
+ ///
+ /// Each test case represents a data type with:
+ /// - `value_in`: A value that appears in both the test array and the IN
list (matches → true)
+ /// - `value_not_in`: A value that appears in the test array but NOT in
the IN list (doesn't match → false)
+ /// - `value_in_list`: A value that appears in the IN list but not in the
array (filler value)
+ /// - `null_value`: A null scalar value for NULL handling tests
+ struct InListPrimitiveTestCase {
+ name: &'static str,
+ value_in: ScalarValue,
+ value_not_in: ScalarValue,
+ value_in_list: ScalarValue,
Review Comment:
The previous tests all had mutli-value lists, such as
```rust
// expression: "a not in ("a", "b")"
```
Maybe we could make this something like `values_in: Vec<ScalarValue>`
However, I see there are multi-value tests below too, so maybe this is ok
##########
datafusion/physical-expr/src/expressions/in_list.rs:
##########
@@ -355,6 +386,57 @@ impl InListExpr {
Some(instantiate_static_filter(array)?),
))
}
+
+ /// Create a new InList expression with a static filter for constant list
expressions.
+ ///
+ /// This validates data types, evaluates the list as constants, and uses
specialized
+ /// StaticFilter implementations for better performance (e.g.,
Int32StaticFilter for Int32).
+ ///
+ /// Returns an error if data types don't match or if the list contains
non-constant expressions.
Review Comment:
I don't think this comment is correct -- the code appears to handle the case
of non-constant expressions as well
I think this might also be clearer if it were simply named `try_new()`
rather than `try_from_static_filter`
##########
datafusion/physical-expr/src/expressions/in_list.rs:
##########
@@ -226,31 +226,62 @@ impl StaticFilter for Int32StaticFilter {
}
fn contains(&self, v: &dyn Array, negated: bool) -> Result<BooleanArray> {
+ // Handle dictionary arrays by recursing on the values
+ downcast_dictionary_array! {
+ v => {
+ let values_contains = self.contains(v.values().as_ref(),
negated)?;
+ let result = take(&values_contains, v.keys(), None)?;
+ return Ok(downcast_array(result.as_ref()))
+ }
+ _ => {}
+ }
+
let v = v
.as_primitive_opt::<Int32Type>()
.ok_or_else(|| exec_datafusion_err!("Failed to downcast array"))?;
- let result = match (v.null_count() > 0, negated) {
+ let haystack_has_nulls = self.null_count > 0;
+ let has_nulls = v.null_count() > 0 || haystack_has_nulls;
+
+ let result = match (has_nulls, negated) {
(true, false) => {
- // has nulls, not negated"
- BooleanArray::from_iter(
- v.iter().map(|value| Some(self.values.contains(&value?))),
- )
+ // Either needle or haystack has nulls, not negated
Review Comment:
```suggestion
// needle has nulls, not negated
```
##########
datafusion/physical-expr/src/expressions/in_list.rs:
##########
@@ -698,562 +750,370 @@ mod tests {
}};
}
- #[test]
- fn in_list_utf8() -> Result<()> {
- let schema = Schema::new(vec![Field::new("a", DataType::Utf8, true)]);
- let a = StringArray::from(vec![Some("a"), Some("d"), None]);
- let col_a = col("a", &schema)?;
- let batch = RecordBatch::try_new(Arc::new(schema.clone()),
vec![Arc::new(a)])?;
-
- // expression: "a in ("a", "b")"
- let list = vec![lit("a"), lit("b")];
- in_list!(
- batch,
- list,
- &false,
- vec![Some(true), Some(false), None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a not in ("a", "b")"
- let list = vec![lit("a"), lit("b")];
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), Some(true), None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a in ("a", "b", null)"
- let list = vec![lit("a"), lit("b"), lit(ScalarValue::Utf8(None))];
- in_list!(
- batch,
- list,
- &false,
- vec![Some(true), None, None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a not in ("a", "b", null)"
- let list = vec![lit("a"), lit("b"), lit(ScalarValue::Utf8(None))];
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), None, None],
- Arc::clone(&col_a),
- &schema
- );
-
- Ok(())
+ /// Test case for primitive types following the standard IN LIST pattern.
+ ///
+ /// Each test case represents a data type with:
+ /// - `value_in`: A value that appears in both the test array and the IN
list (matches → true)
+ /// - `value_not_in`: A value that appears in the test array but NOT in
the IN list (doesn't match → false)
+ /// - `value_in_list`: A value that appears in the IN list but not in the
array (filler value)
+ /// - `null_value`: A null scalar value for NULL handling tests
+ struct InListPrimitiveTestCase {
+ name: &'static str,
+ value_in: ScalarValue,
+ value_not_in: ScalarValue,
+ value_in_list: ScalarValue,
+ null_value: ScalarValue,
}
- #[test]
- fn in_list_binary() -> Result<()> {
- let schema = Schema::new(vec![Field::new("a", DataType::Binary,
true)]);
- let a = BinaryArray::from(vec![
- Some([1, 2, 3].as_slice()),
- Some([1, 2, 2].as_slice()),
- None,
- ]);
- let col_a = col("a", &schema)?;
- let batch = RecordBatch::try_new(Arc::new(schema.clone()),
vec![Arc::new(a)])?;
-
- // expression: "a in ([1, 2, 3], [4, 5, 6])"
- let list = vec![lit([1, 2, 3].as_slice()), lit([4, 5, 6].as_slice())];
- in_list!(
- batch,
- list.clone(),
- &false,
- vec![Some(true), Some(false), None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a not in ([1, 2, 3], [4, 5, 6])"
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), Some(true), None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a in ([1, 2, 3], [4, 5, 6], null)"
- let list = vec![
- lit([1, 2, 3].as_slice()),
- lit([4, 5, 6].as_slice()),
- lit(ScalarValue::Binary(None)),
- ];
- in_list!(
- batch,
- list.clone(),
- &false,
- vec![Some(true), None, None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a in ([1, 2, 3], [4, 5, 6], null)"
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), None, None],
- Arc::clone(&col_a),
- &schema
- );
-
- Ok(())
+ /// Generic test data struct for primitive types.
+ ///
+ /// Holds the three test values needed for IN LIST tests, allowing the data
+ /// to be declared explicitly and reused across multiple types.
+ #[derive(Clone)]
+ struct PrimitiveTestCaseData<T> {
+ value_in: T,
+ value_not_in: T,
+ value_in_list: T,
}
- #[test]
- fn in_list_int64() -> Result<()> {
- let schema = Schema::new(vec![Field::new("a", DataType::Int64, true)]);
- let a = Int64Array::from(vec![Some(0), Some(2), None]);
- let col_a = col("a", &schema)?;
- let batch = RecordBatch::try_new(Arc::new(schema.clone()),
vec![Arc::new(a)])?;
-
- // expression: "a in (0, 1)"
- let list = vec![lit(0i64), lit(1i64)];
- in_list!(
- batch,
- list,
- &false,
- vec![Some(true), Some(false), None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a not in (0, 1)"
- let list = vec![lit(0i64), lit(1i64)];
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), Some(true), None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a in (0, 1, NULL)"
- let list = vec![lit(0i64), lit(1i64), lit(ScalarValue::Null)];
- in_list!(
- batch,
- list,
- &false,
- vec![Some(true), None, None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a not in (0, 1, NULL)"
- let list = vec![lit(0i64), lit(1i64), lit(ScalarValue::Null)];
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), None, None],
- Arc::clone(&col_a),
- &schema
- );
-
- Ok(())
+ /// Helper to create test cases for any primitive type using generic data.
+ ///
+ /// Uses TryInto for flexible type conversion, allowing test data to be
+ /// declared in any convertible type (e.g., i32 for all integer types).
+ fn primitive_test_case<T, D, F>(
+ name: &'static str,
+ constructor: F,
+ data: PrimitiveTestCaseData<D>,
+ ) -> InListPrimitiveTestCase
+ where
+ D: TryInto<T>,
+ <D as TryInto<T>>::Error: Debug,
+ F: Fn(Option<T>) -> ScalarValue,
+ T: Clone,
+ {
+ InListPrimitiveTestCase {
+ name,
+ value_in: constructor(Some(data.value_in.try_into().unwrap())),
+ value_not_in:
constructor(Some(data.value_not_in.try_into().unwrap())),
+ value_in_list:
constructor(Some(data.value_in_list.try_into().unwrap())),
+ null_value: constructor(None),
+ }
}
- #[test]
- fn in_list_float64() -> Result<()> {
- let schema = Schema::new(vec![Field::new("a", DataType::Float64,
true)]);
- let a = Float64Array::from(vec![
- Some(0.0),
- Some(0.2),
- None,
- Some(f64::NAN),
- Some(-f64::NAN),
- ]);
- let col_a = col("a", &schema)?;
- let batch = RecordBatch::try_new(Arc::new(schema.clone()),
vec![Arc::new(a)])?;
-
- // expression: "a in (0.0, 0.1)"
- let list = vec![lit(0.0f64), lit(0.1f64)];
- in_list!(
- batch,
- list,
- &false,
- vec![Some(true), Some(false), None, Some(false), Some(false)],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a not in (0.0, 0.1)"
- let list = vec![lit(0.0f64), lit(0.1f64)];
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), Some(true), None, Some(true), Some(true)],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a in (0.0, 0.1, NULL)"
- let list = vec![lit(0.0f64), lit(0.1f64), lit(ScalarValue::Null)];
- in_list!(
- batch,
- list,
- &false,
- vec![Some(true), None, None, None, None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a not in (0.0, 0.1, NULL)"
- let list = vec![lit(0.0f64), lit(0.1f64), lit(ScalarValue::Null)];
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), None, None, None, None],
- Arc::clone(&col_a),
- &schema
- );
+ /// Runs test cases for multiple types, providing detailed SQL error
messages on failure.
+ ///
+ /// For each test case, runs 4 standard IN LIST scenarios and provides
context
+ /// about the test data and expected behavior when assertions fail.
+ fn run_test_cases(test_cases: Vec<InListPrimitiveTestCase>) -> Result<()> {
+ for test_case in test_cases {
+ let test_name = test_case.name;
+
+ // Get the data type from the scalar value
+ let data_type = test_case.value_in.data_type();
+ let schema = Schema::new(vec![Field::new("a", data_type.clone(),
true)]);
+
+ // Create array from scalar values: [value_in, value_not_in, None]
+ let array = ScalarValue::iter_to_array(vec![
+ test_case.value_in.clone(),
+ test_case.value_not_in.clone(),
+ test_case.null_value.clone(),
+ ])?;
+
+ let col_a = col("a", &schema)?;
+ let batch =
+ RecordBatch::try_new(Arc::new(schema.clone()),
vec![Arc::clone(&array)])?;
+
+ // Helper to format SQL-like representation for error messages
+ let _format_sql = |negated: bool, with_null: bool| -> String {
Review Comment:
this helper seems unused (why is it named starting with `_` 🤔 )
##########
datafusion/physical-expr/src/expressions/in_list.rs:
##########
@@ -698,562 +750,370 @@ mod tests {
}};
}
- #[test]
- fn in_list_utf8() -> Result<()> {
- let schema = Schema::new(vec![Field::new("a", DataType::Utf8, true)]);
- let a = StringArray::from(vec![Some("a"), Some("d"), None]);
- let col_a = col("a", &schema)?;
- let batch = RecordBatch::try_new(Arc::new(schema.clone()),
vec![Arc::new(a)])?;
-
- // expression: "a in ("a", "b")"
- let list = vec![lit("a"), lit("b")];
- in_list!(
- batch,
- list,
- &false,
- vec![Some(true), Some(false), None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a not in ("a", "b")"
- let list = vec![lit("a"), lit("b")];
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), Some(true), None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a in ("a", "b", null)"
- let list = vec![lit("a"), lit("b"), lit(ScalarValue::Utf8(None))];
- in_list!(
- batch,
- list,
- &false,
- vec![Some(true), None, None],
- Arc::clone(&col_a),
- &schema
- );
-
- // expression: "a not in ("a", "b", null)"
- let list = vec![lit("a"), lit("b"), lit(ScalarValue::Utf8(None))];
- in_list!(
- batch,
- list,
- &true,
- vec![Some(false), None, None],
- Arc::clone(&col_a),
- &schema
- );
-
- Ok(())
+ /// Test case for primitive types following the standard IN LIST pattern.
+ ///
+ /// Each test case represents a data type with:
+ /// - `value_in`: A value that appears in both the test array and the IN
list (matches → true)
+ /// - `value_not_in`: A value that appears in the test array but NOT in
the IN list (doesn't match → false)
+ /// - `value_in_list`: A value that appears in the IN list but not in the
array (filler value)
+ /// - `null_value`: A null scalar value for NULL handling tests
+ struct InListPrimitiveTestCase {
+ name: &'static str,
+ value_in: ScalarValue,
+ value_not_in: ScalarValue,
+ value_in_list: ScalarValue,
Review Comment:
my reading of these tests is that they always test a one element list -- as
in the list does not have multiple values
Do you think we need coverage for multi-value lists?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]