This is an automated email from the ASF dual-hosted git repository.
chengchengjin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-gluten.git
The following commit(s) were added to refs/heads/main by this push:
new 4d88d6719c [GLUTEN-8953][VL] Support Iceberg overwrite table #10514
4d88d6719c is described below
commit 4d88d6719c2f7d0011633b43ccc2d913e7e5e8bd
Author: Zouxxyy <[email protected]>
AuthorDate: Mon Aug 25 17:06:11 2025 +0800
[GLUTEN-8953][VL] Support Iceberg overwrite table #10514
---
...taExec.scala => AbstractIcebergWriteExec.scala} | 22 ++----------
.../gluten/execution/OffloadIcebergWrite.scala | 15 ++++++--
.../execution/VeloxIcebergAppendDataExec.scala | 27 ++------------
.../VeloxIcebergOverwriteByExpressionExec.scala | 41 ++++++++++++++++++++++
.../execution/VeloxIcebergReplaceDataExec.scala | 25 ++-----------
.../execution/enhanced/VeloxIcebergSuite.scala | 33 +++++++++++++++--
.../gluten/backendsapi/velox/VeloxBackend.scala | 3 ++
docs/Configuration.md | 1 +
...AppendDataExec.scala => IcebergWriteExec.scala} | 2 +-
.../gluten/backendsapi/BackendSettingsApi.scala | 2 ++
.../org/apache/gluten/config/GlutenConfig.scala | 9 +++++
...V2Exec.scala => ColumnarV2TableWriteExec.scala} | 2 +-
.../extension/columnar/validator/Validators.scala | 3 +-
13 files changed, 109 insertions(+), 76 deletions(-)
diff --git
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergAppendDataExec.scala
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/AbstractIcebergWriteExec.scala
similarity index 68%
copy from
backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergAppendDataExec.scala
copy to
backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/AbstractIcebergWriteExec.scala
index 1fd5374272..3ac518cf76 100644
---
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergAppendDataExec.scala
+++
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/AbstractIcebergWriteExec.scala
@@ -19,21 +19,14 @@ package org.apache.gluten.execution
import org.apache.gluten.IcebergNestedFieldVisitor
import org.apache.gluten.connector.write.{ColumnarBatchDataWriterFactory,
IcebergDataWriteFactory}
-import org.apache.spark.sql.connector.write.Write
-import org.apache.spark.sql.execution.SparkPlan
-import org.apache.spark.sql.execution.datasources.v2._
import org.apache.spark.sql.types.StructType
import org.apache.iceberg.spark.source.IcebergWriteUtil
import org.apache.iceberg.types.TypeUtil
-case class VeloxIcebergAppendDataExec(query: SparkPlan, refreshCache: () =>
Unit, write: Write)
- extends IcebergAppendDataExec {
+abstract class AbstractIcebergWriteExec extends IcebergWriteExec {
- override protected def withNewChildInternal(newChild: SparkPlan):
IcebergAppendDataExec =
- copy(query = newChild)
-
- override def createFactory(schema: StructType):
ColumnarBatchDataWriterFactory = {
+ override protected def createFactory(schema: StructType):
ColumnarBatchDataWriterFactory = {
val writeSchema = IcebergWriteUtil.getWriteSchema(write)
val nestedField = TypeUtil.visit(writeSchema, new
IcebergNestedFieldVisitor)
IcebergDataWriteFactory(
@@ -47,14 +40,3 @@ case class VeloxIcebergAppendDataExec(query: SparkPlan,
refreshCache: () => Unit
)
}
}
-
-object VeloxIcebergAppendDataExec {
- def apply(original: AppendDataExec): IcebergAppendDataExec = {
- VeloxIcebergAppendDataExec(
- original.query,
- original.refreshCache,
- original.write
- )
- }
-
-}
diff --git
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/OffloadIcebergWrite.scala
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/OffloadIcebergWrite.scala
index f8f7abdaa4..1e2beff366 100644
---
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/OffloadIcebergWrite.scala
+++
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/OffloadIcebergWrite.scala
@@ -24,7 +24,7 @@ import
org.apache.gluten.extension.columnar.validator.Validators
import org.apache.gluten.extension.injector.Injector
import org.apache.spark.sql.execution.SparkPlan
-import org.apache.spark.sql.execution.datasources.v2.{AppendDataExec,
ReplaceDataExec}
+import org.apache.spark.sql.execution.datasources.v2.{AppendDataExec,
OverwriteByExpressionExec, ReplaceDataExec}
case class OffloadIcebergWrite() extends OffloadSingleNode {
override def offload(plan: SparkPlan): SparkPlan = plan match {
@@ -42,12 +42,20 @@ case class OffloadIcebergDelete() extends OffloadSingleNode
{
}
}
+case class OffloadIcebergOverwrite() extends OffloadSingleNode {
+ override def offload(plan: SparkPlan): SparkPlan = plan match {
+ case r: OverwriteByExpressionExec =>
+ VeloxIcebergOverwriteByExpressionExec(r)
+ case other => other
+ }
+}
+
object OffloadIcebergWrite {
def inject(injector: Injector): Unit = {
// Inject legacy rule.
injector.gluten.legacy.injectTransform {
c =>
- val offload = Seq(OffloadIcebergWrite(), OffloadIcebergDelete())
+ val offload = Seq(OffloadIcebergWrite(), OffloadIcebergDelete(),
OffloadIcebergOverwrite())
HeuristicTransform.Simple(
Validators.newValidator(new GlutenConfig(c.sqlConf), offload),
offload
@@ -56,7 +64,8 @@ object OffloadIcebergWrite {
val offloads: Seq[RasOffload] = Seq(
RasOffload.from[AppendDataExec](OffloadIcebergWrite()),
- RasOffload.from[ReplaceDataExec](OffloadIcebergDelete())
+ RasOffload.from[ReplaceDataExec](OffloadIcebergDelete()),
+ RasOffload.from[OverwriteByExpressionExec](OffloadIcebergOverwrite())
)
offloads.foreach(
offload =>
diff --git
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergAppendDataExec.scala
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergAppendDataExec.scala
index 1fd5374272..013df1e476 100644
---
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergAppendDataExec.scala
+++
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergAppendDataExec.scala
@@ -16,40 +16,19 @@
*/
package org.apache.gluten.execution
-import org.apache.gluten.IcebergNestedFieldVisitor
-import org.apache.gluten.connector.write.{ColumnarBatchDataWriterFactory,
IcebergDataWriteFactory}
-
import org.apache.spark.sql.connector.write.Write
import org.apache.spark.sql.execution.SparkPlan
import org.apache.spark.sql.execution.datasources.v2._
-import org.apache.spark.sql.types.StructType
-
-import org.apache.iceberg.spark.source.IcebergWriteUtil
-import org.apache.iceberg.types.TypeUtil
case class VeloxIcebergAppendDataExec(query: SparkPlan, refreshCache: () =>
Unit, write: Write)
- extends IcebergAppendDataExec {
+ extends AbstractIcebergWriteExec {
- override protected def withNewChildInternal(newChild: SparkPlan):
IcebergAppendDataExec =
+ override protected def withNewChildInternal(newChild: SparkPlan):
IcebergWriteExec =
copy(query = newChild)
-
- override def createFactory(schema: StructType):
ColumnarBatchDataWriterFactory = {
- val writeSchema = IcebergWriteUtil.getWriteSchema(write)
- val nestedField = TypeUtil.visit(writeSchema, new
IcebergNestedFieldVisitor)
- IcebergDataWriteFactory(
- schema,
- getFileFormat(IcebergWriteUtil.getFileFormat(write)),
- IcebergWriteUtil.getDirectory(write),
- getCodec,
- getPartitionSpec,
- IcebergWriteUtil.getSortOrder(write),
- nestedField
- )
- }
}
object VeloxIcebergAppendDataExec {
- def apply(original: AppendDataExec): IcebergAppendDataExec = {
+ def apply(original: AppendDataExec): IcebergWriteExec = {
VeloxIcebergAppendDataExec(
original.query,
original.refreshCache,
diff --git
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergOverwriteByExpressionExec.scala
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergOverwriteByExpressionExec.scala
new file mode 100644
index 0000000000..00f2160513
--- /dev/null
+++
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergOverwriteByExpressionExec.scala
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+package org.apache.gluten.execution
+
+import org.apache.spark.sql.connector.write.Write
+import org.apache.spark.sql.execution.SparkPlan
+import org.apache.spark.sql.execution.datasources.v2.OverwriteByExpressionExec
+
+case class VeloxIcebergOverwriteByExpressionExec(
+ query: SparkPlan,
+ refreshCache: () => Unit,
+ write: Write)
+ extends AbstractIcebergWriteExec {
+
+ override protected def withNewChildInternal(newChild: SparkPlan):
IcebergWriteExec =
+ copy(query = newChild)
+}
+
+object VeloxIcebergOverwriteByExpressionExec {
+ def apply(original: OverwriteByExpressionExec): IcebergWriteExec = {
+ VeloxIcebergOverwriteByExpressionExec(
+ original.query,
+ original.refreshCache,
+ original.write
+ )
+ }
+}
diff --git
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergReplaceDataExec.scala
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergReplaceDataExec.scala
index 2a6df03e8a..c4f20e4fd1 100644
---
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergReplaceDataExec.scala
+++
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/VeloxIcebergReplaceDataExec.scala
@@ -16,36 +16,15 @@
*/
package org.apache.gluten.execution
-import org.apache.gluten.IcebergNestedFieldVisitor
-import org.apache.gluten.connector.write.{ColumnarBatchDataWriterFactory,
IcebergDataWriteFactory}
-
import org.apache.spark.sql.connector.write.Write
import org.apache.spark.sql.execution.SparkPlan
import org.apache.spark.sql.execution.datasources.v2._
-import org.apache.spark.sql.types.StructType
-
-import org.apache.iceberg.spark.source.IcebergWriteUtil
-import org.apache.iceberg.types.TypeUtil
case class VeloxIcebergReplaceDataExec(query: SparkPlan, refreshCache: () =>
Unit, write: Write)
- extends IcebergAppendDataExec {
+ extends AbstractIcebergWriteExec {
- override protected def withNewChildInternal(newChild: SparkPlan):
IcebergAppendDataExec =
+ override protected def withNewChildInternal(newChild: SparkPlan):
IcebergWriteExec =
copy(query = newChild)
-
- override def createFactory(schema: StructType):
ColumnarBatchDataWriterFactory = {
- val writeSchema = IcebergWriteUtil.getWriteSchema(write)
- val nestedField = TypeUtil.visit(writeSchema, new
IcebergNestedFieldVisitor)
- IcebergDataWriteFactory(
- schema,
- getFileFormat(IcebergWriteUtil.getFileFormat(write)),
- IcebergWriteUtil.getDirectory(write),
- getCodec,
- getPartitionSpec,
- IcebergWriteUtil.getSortOrder(write),
- nestedField
- )
- }
}
object VeloxIcebergReplaceDataExec {
diff --git
a/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
b/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
index 06c8ee0c0a..ee228d5120 100644
---
a/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
+++
b/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
@@ -16,7 +16,7 @@
*/
package org.apache.gluten.execution.enhanced
-import org.apache.gluten.execution.{IcebergAppendDataExec, IcebergSuite,
VeloxIcebergReplaceDataExec}
+import org.apache.gluten.execution.{IcebergSuite, VeloxIcebergAppendDataExec,
VeloxIcebergOverwriteByExpressionExec, VeloxIcebergReplaceDataExec}
import org.apache.gluten.tags.EnhancedFeaturesTest
import org.apache.spark.sql.execution.CommandResultExec
@@ -36,7 +36,7 @@ class VeloxIcebergSuite extends IcebergSuite {
df.queryExecution.executedPlan
.asInstanceOf[CommandResultExec]
.commandPhysicalPlan
- .isInstanceOf[IcebergAppendDataExec])
+ .isInstanceOf[VeloxIcebergAppendDataExec])
val selectDf = spark.sql("""
|select * from iceberg_tb2;
|""".stripMargin)
@@ -61,7 +61,7 @@ class VeloxIcebergSuite extends IcebergSuite {
df.queryExecution.executedPlan
.asInstanceOf[CommandResultExec]
.commandPhysicalPlan
- .isInstanceOf[IcebergAppendDataExec])
+ .isInstanceOf[VeloxIcebergAppendDataExec])
val selectDf = spark.sql("""
|select * from iceberg_tb2;
|""".stripMargin)
@@ -113,4 +113,31 @@ class VeloxIcebergSuite extends IcebergSuite {
assert(result.length == 5)
}
}
+
+ test("iceberg insert overwrite") {
+ withTable("iceberg_tb2") {
+ spark.sql("""
+ |create table if not exists iceberg_tb2(a int) using iceberg
+ |""".stripMargin)
+
+ spark.sql("insert into table iceberg_tb2 values (1)")
+
+ // Overwrite table
+ val df = spark.sql("""
+ |insert overwrite table iceberg_tb2 values (2)
+ |""".stripMargin)
+ assert(
+ df.queryExecution.executedPlan
+ .asInstanceOf[CommandResultExec]
+ .commandPhysicalPlan
+ .isInstanceOf[VeloxIcebergOverwriteByExpressionExec])
+
+ val selectDf = spark.sql("""
+ |select * from iceberg_tb2;
+ |""".stripMargin)
+ val result = selectDf.collect()
+ assert(result.length == 1)
+ assert(result(0).get(0) == 2)
+ }
+ }
}
diff --git
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
index 9821efa66e..a838897352 100644
---
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
+++
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
@@ -562,4 +562,7 @@ object VeloxBackendSettings extends BackendSettingsApi {
override def supportReplaceDataExec(): Boolean =
GlutenConfig.get.enableReplaceData && enableEnhancedFeatures()
+
+ override def supportOverwriteByExpression(): Boolean =
+ GlutenConfig.get.enableOverwriteByExpression && enableEnhancedFeatures()
}
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 082ae13945..7b9bcbe49a 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -96,6 +96,7 @@ nav_order: 15
| spark.gluten.sql.columnar.limit | true
|
| spark.gluten.sql.columnar.maxBatchSize | 4096
|
| spark.gluten.sql.columnar.numaBinding | false
|
+| spark.gluten.sql.columnar.overwriteByExpression | true
| Enable or disable columnar v2 command overwrite by expression.
[...]
| spark.gluten.sql.columnar.parquet.write.blockSize | 128MB
|
| spark.gluten.sql.columnar.partial.project | true
| Break up one project node into 2 phases when some of the
expressions are non offload-able. Phase one is a regular offloaded project
transformer that evaluates the offload-able expressions in native, phase two
preserves the output from phase one and evaluates the remaining
non-offload-able expressions using vanilla Spark projections
[...]
| spark.gluten.sql.columnar.physicalJoinOptimizationLevel | 12
| Fallback to row operators if there are several continuous joins.
[...]
diff --git
a/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergAppendDataExec.scala
b/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergWriteExec.scala
similarity index 98%
rename from
gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergAppendDataExec.scala
rename to
gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergWriteExec.scala
index 3a21d78b94..0f040f8775 100644
---
a/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergAppendDataExec.scala
+++
b/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergWriteExec.scala
@@ -26,7 +26,7 @@ import org.apache.iceberg.types.Type.TypeID
import scala.collection.JavaConverters._
-trait IcebergAppendDataExec extends ColumnarAppendDataExec {
+trait IcebergWriteExec extends ColumnarV2TableWriteExec {
protected def getFileFormat(format: FileFormat): Int = {
format match {
diff --git
a/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/BackendSettingsApi.scala
b/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/BackendSettingsApi.scala
index f4b9c46df1..a8c2d75fe7 100644
---
a/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/BackendSettingsApi.scala
+++
b/gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/BackendSettingsApi.scala
@@ -163,4 +163,6 @@ trait BackendSettingsApi {
def supportAppendDataExec(): Boolean = false
def supportReplaceDataExec(): Boolean = false
+
+ def supportOverwriteByExpression(): Boolean = false
}
diff --git
a/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
b/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
index 9d91bb6379..ef2bd560f8 100644
---
a/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
+++
b/gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala
@@ -95,6 +95,8 @@ class GlutenConfig(conf: SQLConf) extends
GlutenCoreConfig(conf) {
def enableReplaceData: Boolean = getConf(COLUMNAR_REPLACE_DATA_ENABLED)
+ def enableOverwriteByExpression: Boolean =
getConf(COLUMNAR_OVERWRIET_BY_EXPRESSION_ENABLED)
+
def enableColumnarShuffledHashJoin: Boolean =
getConf(COLUMNAR_SHUFFLED_HASH_JOIN_ENABLED)
def shuffledHashJoinOptimizeBuildSide: Boolean =
@@ -887,6 +889,13 @@ object GlutenConfig {
.booleanConf
.createWithDefault(true)
+ val COLUMNAR_OVERWRIET_BY_EXPRESSION_ENABLED =
+ buildConf("spark.gluten.sql.columnar.overwriteByExpression")
+ .internal()
+ .doc("Enable or disable columnar v2 command overwrite by expression.")
+ .booleanConf
+ .createWithDefault(true)
+
val COLUMNAR_PREFER_STREAMING_AGGREGATE =
buildConf("spark.gluten.sql.columnar.preferStreamingAggregate")
.internal()
diff --git
a/gluten-substrait/src/main/scala/org/apache/gluten/execution/ColumnarWriteToDatasourceV2Exec.scala
b/gluten-substrait/src/main/scala/org/apache/gluten/execution/ColumnarV2TableWriteExec.scala
similarity index 98%
rename from
gluten-substrait/src/main/scala/org/apache/gluten/execution/ColumnarWriteToDatasourceV2Exec.scala
rename to
gluten-substrait/src/main/scala/org/apache/gluten/execution/ColumnarV2TableWriteExec.scala
index d174f5a128..7265814de7 100644
---
a/gluten-substrait/src/main/scala/org/apache/gluten/execution/ColumnarWriteToDatasourceV2Exec.scala
+++
b/gluten-substrait/src/main/scala/org/apache/gluten/execution/ColumnarV2TableWriteExec.scala
@@ -32,7 +32,7 @@ import org.apache.spark.sql.types.StructType
import org.apache.spark.sql.vectorized.ColumnarBatch
import org.apache.spark.util.LongAccumulator
-trait ColumnarAppendDataExec extends V2ExistingTableWriteExec with
ValidatablePlan {
+trait ColumnarV2TableWriteExec extends V2ExistingTableWriteExec with
ValidatablePlan {
protected def createFactory(schema: StructType):
ColumnarBatchDataWriterFactory
diff --git
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/validator/Validators.scala
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/validator/Validators.scala
index 8456fd4a70..ef4a50ee7a 100644
---
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/validator/Validators.scala
+++
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/validator/Validators.scala
@@ -29,7 +29,7 @@ import org.apache.spark.internal.Logging
import org.apache.spark.sql.execution._
import org.apache.spark.sql.execution.aggregate.{HashAggregateExec,
ObjectHashAggregateExec, SortAggregateExec}
import org.apache.spark.sql.execution.datasources.WriteFilesExec
-import org.apache.spark.sql.execution.datasources.v2.{AppendDataExec,
BatchScanExec, ReplaceDataExec}
+import org.apache.spark.sql.execution.datasources.v2.{AppendDataExec,
BatchScanExec, OverwriteByExpressionExec, ReplaceDataExec}
import org.apache.spark.sql.execution.exchange.{BroadcastExchangeExec,
ShuffleExchangeExec}
import org.apache.spark.sql.execution.joins._
import org.apache.spark.sql.execution.window.WindowExec
@@ -138,6 +138,7 @@ object Validators {
case p: TakeOrderedAndProjectExec if
!settings.supportColumnarShuffleExec() => fail(p)
case p: AppendDataExec if !settings.supportAppendDataExec() => fail(p)
case p: ReplaceDataExec if !settings.supportReplaceDataExec() => fail(p)
+ case p: OverwriteByExpressionExec if
!settings.supportOverwriteByExpression() => fail(p)
case _ => pass()
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]