Re: [PR] fix: propagate inner-field metadata through composite-type constructors [datafusion]
paleolimbot commented on code in PR #21984:
URL: https://github.com/apache/datafusion/pull/21984#discussion_r3253991816
##
datafusion/functions-nested/src/make_array.rs:
##
@@ -129,7 +161,30 @@ impl ScalarUDFImpl for MakeArray {
/// `make_array_inner` is the implementation of the `make_array` function.
/// Constructs an array using the input `data` as `ArrayRef`.
/// Returns a reference-counted `Array` instance result.
+///
+/// This entry point synthesizes a bare inner field with no metadata. Prefer
+/// [`make_array_inner_with_field`] from contexts where a planning-time
+/// `FieldRef` is available (e.g. `MakeArray::invoke_with_args`) so that
+/// per-field metadata (notably Arrow extension types) flows through.
Review Comment:
I feel like I may be missing something with respect to how these get called
(any place that this gets called otherwise feels like it may be a bug or an
opportunity to propagate metadata that isn't captured in this PR). If this
causes CI mayhem it could be tracked as a follow-up ticket.
##
datafusion/functions-nested/src/map.rs:
##
@@ -63,8 +63,46 @@ fn can_evaluate_to_const(args: &[ColumnarValue]) -> bool {
.all(|arg| matches!(arg, ColumnarValue::Scalar(_)))
}
+#[cfg(test)]
fn make_map_batch(args: &[ColumnarValue]) -> Result {
Review Comment:
Should this move to `mod test`?
##
datafusion/functions-aggregate/src/array_agg.rs:
##
@@ -1204,6 +1246,31 @@ mod tests {
use datafusion_physical_expr::PhysicalExpr;
use datafusion_physical_expr::expressions::Column;
+fn int32_field() -> FieldRef {
+Arc::new(Field::new("c", DataType::Int32, true))
+}
+
+/// Regression test for #21982: `array_agg` must propagate the input
+/// field's metadata onto the resulting list's inner field.
+#[test]
+fn array_agg_preserves_inner_field_metadata() -> Result<()> {
+let metadata = std::collections::HashMap::from([(
+"ARROW:extension:name".to_string(),
+"arrow.uuid".to_string(),
+)]);
+let input: FieldRef =
+Arc::new(Field::new("c", DataType::Int64,
true).with_metadata(metadata));
Review Comment:
This can use arbitrary key/value metadata
##
datafusion/functions/src/core/struct.rs:
##
Review Comment:
This can return internal_err now (I missed some others I think)
##
datafusion/functions/src/core/struct.rs:
##
@@ -137,3 +159,42 @@ impl ScalarUDFImpl for StructFunc {
self.doc()
}
}
+
+#[cfg(test)]
+mod tests {
+use super::*;
+use std::collections::HashMap;
+
+/// Regression test for #21982: `struct(...)` must propagate each input
+/// field's metadata onto the corresponding member of the output struct.
+#[test]
+fn struct_preserves_member_metadata() -> Result<()> {
+let with_meta = |k: &str, v: &str, dt: DataType| -> FieldRef {
+let metadata = HashMap::from([(k.to_string(), v.to_string())]);
+Arc::new(Field::new("c", dt, true).with_metadata(metadata))
+};
+let a = with_meta("ARROW:extension:name", "arrow.uuid",
DataType::Int64);
+let b = with_meta("ARROW:extension:name", "arrow.json",
DataType::Utf8);
Review Comment:
These can probably just be arbitrary key/value metadata and not invalid
extension types
##
datafusion/functions-nested/src/map.rs:
##
@@ -398,8 +456,44 @@ impl ScalarUDFImpl for MapFunc {
))
}
+fn return_field_from_args(&self, args: ReturnFieldArgs) ->
Result {
+let [keys_arg, values_arg] = take_function_args(self.name(),
args.arg_fields)?;
+
+// Element fields preserve the input lists' element-field metadata
+// (e.g. Arrow extension types). Override only the name and nullable
+// flag to match the canonical map entries schema (key non-null,
+// value nullable).
+let key_field = get_element_field(keys_arg)?
+.as_ref()
+.clone()
+.with_name("key")
+.with_nullable(false);
+let value_field = get_element_field(values_arg)?
+.as_ref()
+.clone()
+.with_name("value")
+.with_nullable(true);
+
+let mut builder = SchemaBuilder::new();
+builder.push(key_field);
+builder.push(value_field);
+let fields = builder.finish().fields;
+let entries = Arc::new(Field::new("entries", DataType::Struct(fields),
false));
Review Comment:
Does this work/is this any cleaner?
```suggestion
let entries = Arc::new(Field::new("entries",
DataType::Struct(vec![Arc::new(key_field), Arc::new(value_field)]), false));
```
##
datafusion/functions-nested/src/repeat.rs:
##
Review Comment:
I think this can return `internal_err()` now
##
datafusion/functions-nested/src/arrays_zip.rs:
##
@@ -223,12 +266,21 @@ f
Re: [PR] fix: propagate inner-field metadata through composite-type constructors [datafusion]
adriangb commented on code in PR #21984:
URL: https://github.com/apache/datafusion/pull/21984#discussion_r3250778416
##
datafusion/sqllogictest/test_files/array_metadata_propagation.slt:
##
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Regression tests for https://github.com/apache/datafusion/issues/21982:
+# UDFs/UDAFs that wrap an input column into a composite type (List, Struct,
+# Map, ...) must propagate the input field's metadata onto the inner field
+# of the output composite. The data type rendering used by `arrow_field`
+# includes inner-field metadata, so we verify by string-matching the
+# `data_type` projection.
+
+# make_array preserves inner field metadata
+query T
+SELECT arrow_field(make_array(with_metadata(1, 'k', 'v')))['data_type']
+
+List(Int64, metadata: {"k": "v"})
Review Comment:
Added a version with `VALUES`
--
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]
Re: [PR] fix: propagate inner-field metadata through composite-type constructors [datafusion]
adriangb commented on code in PR #21984:
URL: https://github.com/apache/datafusion/pull/21984#discussion_r3250776729
##
datafusion/functions-nested/src/arrays_zip.rs:
##
@@ -315,15 +361,60 @@ fn arrays_zip_inner(args: &[ArrayRef]) ->
Result {
let null_buffer = null_builder.finish();
-let result = ListArray::try_new(
+let list_inner = inner_field.unwrap_or_else(|| {
Arc::new(Field::new_list_field(
struct_array.data_type().clone(),
true,
-)),
+))
+});
+let result = ListArray::try_new(
+list_inner,
OffsetBuffer::new(offsets.into()),
Arc::new(struct_array),
null_buffer,
)?;
Ok(Arc::new(result))
}
+
+#[cfg(test)]
+mod tests {
+use super::*;
+use std::collections::HashMap;
+
+/// Regression test for #21982: `arrays_zip` must propagate each input
+/// list's element-field metadata onto the corresponding struct member.
+#[test]
+fn arrays_zip_preserves_struct_member_metadata() -> Result<()> {
+let with_meta = |k: &str, v: &str, dt: DataType| -> FieldRef {
+let metadata = HashMap::from([(k.to_string(), v.to_string())]);
+Arc::new(Field::new("e", dt, true).with_metadata(metadata))
+};
+let a_inner = with_meta("ARROW:extension:name", "arrow.uuid",
DataType::Int64);
+let b_inner = with_meta("ARROW:extension:name", "arrow.json",
DataType::Utf8);
Review Comment:
Replaced with arbitrary metadata
--
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]
Re: [PR] fix: propagate inner-field metadata through composite-type constructors [datafusion]
adriangb commented on code in PR #21984:
URL: https://github.com/apache/datafusion/pull/21984#discussion_r3250770842
##
datafusion/functions-nested/src/arrays_zip.rs:
##
@@ -223,12 +264,17 @@ fn arrays_zip_inner(args: &[ArrayRef]) ->
Result {
.map(|v| v.as_ref().map(|view| view.values.to_data()))
.collect();
-let struct_fields: Fields = element_types
-.iter()
-.enumerate()
-.map(|(i, dt)| Field::new(format!("{}", i + 1), dt.clone(), true))
-.collect::>()
-.into();
+// Prefer the planning-time struct fields (which preserve input metadata)
+// when available; fall back to building bare fields from element types.
+let struct_fields: Fields = match inner_field.as_ref().map(|f|
f.data_type()) {
Review Comment:
Yes this should not happen unless people are calling from rust and doing
somewhat weird things.
--
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]
Re: [PR] fix: propagate inner-field metadata through composite-type constructors [datafusion]
adriangb commented on code in PR #21984: URL: https://github.com/apache/datafusion/pull/21984#discussion_r3250767494 ## datafusion/functions-nested/src/arrays_zip.rs: ## Review Comment: Good point. I opened https://github.com/apache/datafusion/issues/5 -- 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]
Re: [PR] fix: propagate inner-field metadata through composite-type constructors [datafusion]
adriangb commented on code in PR #21984:
URL: https://github.com/apache/datafusion/pull/21984#discussion_r3250764360
##
datafusion/functions-nested/src/range.rs:
##
@@ -247,21 +247,73 @@ impl ScalarUDFImpl for Range {
}
}
+fn return_field_from_args(&self, args: ReturnFieldArgs) ->
Result {
+if args.arg_fields.iter().any(|f| f.data_type().is_null()) {
+return Ok(Arc::new(Field::new(self.name(), DataType::Null, true)));
+}
+// Reuse the data type computed by `return_type` so the date64→date32
+// coercion and timezone preservation logic stays in one place. Then
+// walk the result and replace the inner field with a metadata-bearing
+// copy of the start arg's field so extension types flow through.
+let arg_types: Vec<_> = args
Review Comment:
You are right. I removed `range()`
--
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]
Re: [PR] fix: propagate inner-field metadata through composite-type constructors [datafusion]
paleolimbot commented on code in PR #21984:
URL: https://github.com/apache/datafusion/pull/21984#discussion_r3245386025
##
datafusion/functions-nested/src/arrays_zip.rs:
##
@@ -223,12 +264,17 @@ fn arrays_zip_inner(args: &[ArrayRef]) ->
Result {
.map(|v| v.as_ref().map(|view| view.values.to_data()))
.collect();
-let struct_fields: Fields = element_types
-.iter()
-.enumerate()
-.map(|(i, dt)| Field::new(format!("{}", i + 1), dt.clone(), true))
-.collect::>()
-.into();
+// Prefer the planning-time struct fields (which preserve input metadata)
+// when available; fall back to building bare fields from element types.
+let struct_fields: Fields = match inner_field.as_ref().map(|f|
f.data_type()) {
Review Comment:
If this can ever happen it would be helpful for future readers to elaborate
on how (as far as I know this is not supposed to happen?)
##
datafusion/functions-nested/src/arrays_zip.rs:
##
@@ -139,8 +140,45 @@ impl ScalarUDFImpl for ArraysZip {
}
+fn return_field_from_args(&self, args: ReturnFieldArgs) ->
Result {
+if args.arg_fields.is_empty() {
+return exec_err!("arrays_zip requires at least one argument");
+}
+
+// For each input list-typed argument, take its element field and
+// rename it to the positional struct member name (1-based), preserving
+// metadata. For Null-typed inputs, build a bare Null field.
+let struct_fields: Fields = args
+.arg_fields
+.iter()
+.enumerate()
+.map(|(i, arg_field)| {
+let name = format!("{}", i + 1);
+match arg_field.data_type() {
+List(field) | LargeList(field) | FixedSizeList(field, _)
=> {
+Ok(nullable_inner_field_from(field, &name))
+}
+Null => Ok(Arc::new(Field::new(name, Null, true))),
+dt => exec_err!("arrays_zip expects array arguments, got
{dt}"),
+}
+})
+.collect::>>()?
+.into();
+let struct_dt = DataType::Struct(struct_fields);
+let inner = Arc::new(Field::new(Field::LIST_FIELD_DEFAULT_NAME,
struct_dt, true));
+Ok(Arc::new(Field::new(self.name(), List(inner), true)))
+}
+
fn invoke_with_args(&self, args: ScalarFunctionArgs) ->
Result {
-make_scalar_function(arrays_zip_inner)(&args.args)
+let inner_field = match args.return_field.data_type() {
+List(field) | LargeList(field) | FixedSizeList(field, _) => {
+Some(Arc::clone(field))
+}
+_ => None,
+};
Review Comment:
Should this return `internal_err!()` for the case where the return field is
not a `DataType::List`?
##
datafusion/functions-nested/src/make_array.rs:
##
@@ -105,8 +107,34 @@ impl ScalarUDFImpl for MakeArray {
Ok(DataType::new_list(element_type, true))
}
+fn return_field_from_args(&self, args: ReturnFieldArgs) ->
Result {
+// Pick the first non-Null argument's field as the source of element
+// type and metadata; fall back to Null if all inputs are Null.
+// Coercion has already unified element types, so any non-Null input is
+// representative.
+let inner = args
+.arg_fields
+.iter()
+.find(|f| !f.data_type().is_null())
+.map(nullable_list_item_field_from)
+.unwrap_or_else(|| Arc::new(Field::new_list_field(Null, true)));
+Ok(Arc::new(Field::new(
+self.name(),
+DataType::List(inner),
+true,
+)))
+}
+
fn invoke_with_args(&self, args: ScalarFunctionArgs) ->
Result {
-make_scalar_function(make_array_inner)(&args.args)
+let inner_field = match args.return_field.data_type() {
+DataType::List(field)
+| DataType::LargeList(field)
+| DataType::FixedSizeList(field, _) => Some(Arc::clone(field)),
+_ => None,
Review Comment:
Should this return an internal_err for the case where the return type wasn't
computed to be a list?
##
datafusion/functions-nested/src/map.rs:
##
@@ -672,6 +772,60 @@ fn build_map_array(
#[cfg(test)]
mod tests {
use super::*;
+
+/// Regression test for #21982: `map(...)` must propagate the input list
+/// elements' metadata onto the map's `key` and `value` fields.
+#[test]
+fn map_preserves_key_value_field_metadata() -> Result<()> {
+use datafusion_expr::ReturnFieldArgs;
+
+let key_inner: FieldRef =
+Arc::new(Field::new("e", DataType::Utf8, false).with_metadata(
+std::collections::HashMap::from([(
+"ARROW:extension:name".to_string(),
+"arrow.uuid"
Re: [PR] fix: propagate inner-field metadata through composite-type constructors [datafusion]
adriangb commented on PR #21984: URL: https://github.com/apache/datafusion/pull/21984#issuecomment-4456103665 @paleolimbot I wonder if you’d have time to review this PR? I think these are places where we *should* preserve metadata -- 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]
Re: [PR] fix: propagate inner-field metadata through composite-type constructors [datafusion]
github-actions[bot] commented on PR #21984: URL: https://github.com/apache/datafusion/pull/21984#issuecomment-4363214775 Thank you for opening this pull request! Reviewer note: [cargo-semver-checks](https://github.com/obi1kenobi/cargo-semver-checks) reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch). Details ``` Cloning origin/main Building datafusion-common v53.1.0 (current) error: running cargo-doc on crate 'datafusion-common' failed with output: - Compiling proc-macro2 v1.0.106 Compiling quote v1.0.45 Compiling unicode-ident v1.0.24 Compiling libc v0.2.186 Compiling autocfg v1.5.0 Compiling libm v0.2.16 Checking cfg-if v1.0.4 Compiling num-traits v0.2.19 Checking memchr v2.8.0 Compiling syn v2.0.117 Compiling find-msvc-tools v0.1.9 Compiling shlex v1.3.0 Compiling serde_core v1.0.228 Compiling zerocopy v0.8.48 Checking itoa v1.0.18 Compiling jobserver v0.1.34 Compiling zmij v1.0.21 Checking bytes v1.11.1 Compiling cc v1.2.61 Compiling serde_json v1.0.149 Checking num-integer v0.1.46 Checking stable_deref_trait v1.2.1 Checking iana-time-zone v0.1.65 Compiling version_check v0.9.5 Checking siphasher v1.0.2 Compiling getrandom v0.3.4 Checking phf_shared v0.12.1 Compiling ahash v0.8.12 Checking chrono v0.4.44 Checking num-bigint v0.4.6 Compiling chrono-tz v0.10.4 Checking phf v0.12.1 Checking once_cell v1.21.4 Compiling synstructure v0.13.2 Checking arrow-schema v58.1.0 Checking num-complex v0.4.6 Checking hashbrown v0.16.1 Compiling pkg-config v0.3.33 Checking litemap v0.8.2 Checking writeable v0.6.3 Checking lexical-util v1.0.7 Compiling zstd-sys v2.0.16+zstd.1.5.7 Checking smallvec v1.15.1 Compiling zerocopy-derive v0.8.48 Compiling zerofrom-derive v0.1.7 Compiling yoke-derive v0.8.2 Compiling zerovec-derive v0.11.3 Checking zerofrom v0.1.7 Checking yoke v0.8.2 Compiling displaydoc v0.2.5 Checking zerotrie v0.2.4 Checking utf8_iter v1.0.4 Checking zerovec v0.11.6 Compiling icu_normalizer_data v2.2.0 Compiling object v0.37.3 Compiling icu_properties_data v2.2.0 Checking tinystr v0.8.3 Checking icu_locale_core v2.2.0 Checking potential_utf v0.1.5 Checking icu_provider v2.2.0 Checking icu_collections v2.2.0 Compiling semver v1.0.28 Compiling rustc_version v0.4.1 Checking lexical-write-integer v1.0.6 Checking lexical-parse-integer v1.0.6 Compiling zstd-safe v7.2.4 Checking lexical-parse-float v1.0.6 Checking lexical-write-float v1.0.6 Checking half v2.7.1 Checking icu_properties v2.2.0 Checking arrow-buffer v58.1.0 Checking arrow-data v58.1.0 Checking icu_normalizer v2.2.0 Checking arrow-array v58.1.0 Compiling flatbuffers v25.12.19 Checking aho-corasick v1.1.4 Compiling ar_archive_writer v0.5.1 Checking futures-core v0.3.32 Checking pin-project-lite v0.2.17 Checking unicode-segmentation v1.13.2 Checking unicode-width v0.2.2 Checking ryu v1.0.23 Compiling parking_lot_core v0.9.12 Checking futures-sink v0.3.32 Checking base64 v0.22.1 Checking regex-syntax v0.8.10 Checking arrow-select v58.1.0 Checking futures-channel v0.3.32 Compiling psm v0.1.31 Checking comfy-table v7.2.2 Checking idna_adapter v1.2.2 Checking lexical-core v1.0.6 Checking arrow-ord v58.1.0 Compiling futures-macro v0.3.32 Checking atoi v2.0.0 Checking twox-hash v2.1.2 Checking scopeguard v1.2.0 Checking regex-automata v0.4.14 Checking slab v0.4.12 Checking futures-task v0.3.32 Checking percent-encoding v2.3.2 Checking allocator-api2 v0.2.21 Checking alloc-no-stdlib v2.0.4 Checking futures-io v0.3.32 Compiling thiserror v2.0.18 Checking equivalent v1.0.2 Checking bitflags v2.11.1 Checking foldhash v0.2.0 Checking hashbrown v0.17.0 Checking futures-util v0.3.32 Checking alloc-stdlib v0.2.2 Checking form_urlencoded v1.2.2 Checking lock_api v0.4.14 Checking lz4_flex v0.13.0 Checking regex v1.12.3 Checking arrow-cast v58.1.0 Checking idna v1.1.0 Compiling thiserror-impl v2.0.18 Compiling stacker v0.1.24 Compiling ring v0.17.14 Checking csv-core v0.1.13 Compiling snap v1.1.1 Checking simdutf8 v0.1.5 Compiling getrandom v0.4.2 Checking either v1.15.0 Compiling paste v1.0.15 Check
Re: [PR] fix: propagate inner-field metadata through composite-type constructors [datafusion]
Copilot commented on code in PR #21984:
URL: https://github.com/apache/datafusion/pull/21984#discussion_r3176272335
##
datafusion/spark/src/function/aggregate/collect.rs:
##
@@ -85,7 +85,7 @@ impl AggregateUDFImpl for SparkCollectList {
let data_type = field.data_type().clone();
let ignore_nulls = true;
Ok(Box::new(NullToEmptyListAccumulator::new(
-ArrayAggAccumulator::try_new(&data_type, ignore_nulls)?,
+ArrayAggAccumulator::try_new(field, ignore_nulls)?,
data_type,
)))
Review Comment:
`collect_list` now passes `field` into `ArrayAggAccumulator`, but metadata
can still be dropped when `NullToEmptyListAccumulator` converts a NULL result
into an empty list: it builds the empty list from `self.data_type` (a
`DataType` with no field metadata) via
`SingleRowListArrayBuilder::new(empty_array)`, so extension-type metadata won’t
round-trip for the all-NULL case. Consider storing the input `FieldRef` (or at
least the planned list inner `Field`) in `NullToEmptyListAccumulator` and using
`SingleRowListArrayBuilder::with_field(...)` (or `list_inner_field_from`) when
constructing the empty list. Also consider overriding `return_field`/updating
`state_fields` for `collect_list` to propagate inner-field metadata at planning
time (similar to `array_agg`).
##
datafusion/spark/src/function/aggregate/collect.rs:
##
@@ -143,7 +143,7 @@ impl AggregateUDFImpl for SparkCollectSet {
let data_type = field.data_type().clone();
let ignore_nulls = true;
Ok(Box::new(NullToEmptyListAccumulator::new(
-DistinctArrayAggAccumulator::try_new(&data_type, None,
ignore_nulls)?,
+DistinctArrayAggAccumulator::try_new(field, None, ignore_nulls)?,
data_type,
)))
Review Comment:
Same issue as `collect_list`: when `collect_set` produces a NULL result and
`NullToEmptyListAccumulator` converts it to an empty list, the empty list is
built without access to the input `FieldRef` metadata, so extension-type
metadata is lost for the all-NULL case. Consider threading the input `FieldRef`
(or planned inner `Field`) through the wrapper and using it when building the
empty list scalar; also consider overriding `return_field`/updating
`state_fields` so planning-time types retain inner metadata.
--
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]
