drill-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From amansi...@apache.org
Subject drill git commit: DRILL-2092: For HashJoin and MergeJoin, process the null comparisons for IS NOT DISTINCT FROM operator.
Date Thu, 05 Feb 2015 00:12:50 GMT
Repository: drill
Updated Branches:
  refs/heads/master fd005d276 -> cafb7d4ff


DRILL-2092:  For HashJoin and MergeJoin, process the null comparisons for IS NOT DISTINCT
FROM operator.

Added test input and modified test to use baseline CSV.

Modified comparator string to reflect calcite's SqlKind.


Project: http://git-wip-us.apache.org/repos/asf/drill/repo
Commit: http://git-wip-us.apache.org/repos/asf/drill/commit/cafb7d4f
Tree: http://git-wip-us.apache.org/repos/asf/drill/tree/cafb7d4f
Diff: http://git-wip-us.apache.org/repos/asf/drill/diff/cafb7d4f

Branch: refs/heads/master
Commit: cafb7d4ff2c5ed58bc9985e3a3916ff8fb6b9a78
Parents: fd005d2
Author: Aman Sinha <asinha@maprtech.com>
Authored: Sat Jan 31 18:19:46 2015 -0800
Committer: Aman Sinha <asinha@maprtech.com>
Committed: Wed Feb 4 15:32:26 2015 -0800

----------------------------------------------------------------------
 .../physical/impl/common/ChainedHashTable.java  |  3 +-
 .../exec/physical/impl/join/HashJoinBatch.java  | 11 ++--
 .../exec/physical/impl/join/JoinUtils.java      | 54 ++++++++++++++++++++
 .../exec/physical/impl/join/MergeJoinBatch.java | 12 ++++-
 .../exec/planner/physical/HashJoinPrel.java     |  4 +-
 .../drill/exec/planner/physical/JoinPrel.java   | 44 ++++++++++++++++
 .../exec/planner/physical/MergeJoinPrel.java    |  4 +-
 .../exec/fn/impl/TestAggregateFunctions.java    | 38 ++++++++++++++
 .../resources/agg/bugs/drill2092/input.json     | 15 ++++++
 .../resources/agg/bugs/drill2092/result.tsv     |  7 +++
 10 files changed, 181 insertions(+), 11 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/drill/blob/cafb7d4f/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/common/ChainedHashTable.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/common/ChainedHashTable.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/common/ChainedHashTable.java
index fd6a3e2..ea19645 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/common/ChainedHashTable.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/common/ChainedHashTable.java
@@ -125,7 +125,8 @@ public class ChainedHashTable {
   private final boolean areNullsEqual;
 
   public ChainedHashTable(HashTableConfig htConfig, FragmentContext context, BufferAllocator
allocator,
-                          RecordBatch incomingBuild, RecordBatch incomingProbe, RecordBatch
outgoing, boolean areNullsEqual) {
+                          RecordBatch incomingBuild, RecordBatch incomingProbe, RecordBatch
outgoing,
+                          boolean areNullsEqual) {
 
     this.htConfig = htConfig;
     this.context = context;

http://git-wip-us.apache.org/repos/asf/drill/blob/cafb7d4f/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/HashJoinBatch.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/HashJoinBatch.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/HashJoinBatch.java
index 4af0292..1963e98 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/HashJoinBatch.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/HashJoinBatch.java
@@ -42,6 +42,7 @@ import org.apache.drill.exec.physical.impl.common.HashTable;
 import org.apache.drill.exec.physical.impl.common.HashTableConfig;
 import org.apache.drill.exec.physical.impl.common.HashTableStats;
 import org.apache.drill.exec.physical.impl.common.IndexPointer;
+import org.apache.drill.exec.physical.impl.join.JoinUtils.JoinComparator;
 import org.apache.drill.exec.physical.impl.sort.RecordBatchData;
 import org.apache.drill.exec.record.AbstractRecordBatch;
 import org.apache.drill.exec.record.BatchSchema;
@@ -281,15 +282,19 @@ public class HashJoinBatch extends AbstractRecordBatch<HashJoinPOP>
{
     NamedExpression rightExpr[] = new NamedExpression[conditionsSize];
     NamedExpression leftExpr[] = new NamedExpression[conditionsSize];
 
+    JoinComparator comparator = JoinComparator.NONE;
     // Create named expressions from the conditions
     for (int i = 0; i < conditionsSize; i++) {
       rightExpr[i] = new NamedExpression(conditions.get(i).getRight(), new FieldReference("build_side_"
+ i));
       leftExpr[i] = new NamedExpression(conditions.get(i).getLeft(), new FieldReference("probe_side_"
+ i));
 
-      // Hash join only supports equality currently.
-      assert conditions.get(i).getRelationship().equals("==");
+      // Hash join only supports certain types of comparisons
+      comparator = JoinUtils.checkAndSetComparison(conditions.get(i), comparator);
     }
 
+    assert comparator != JoinComparator.NONE;
+    boolean areNullsEqual = (comparator == JoinComparator.IS_NOT_DISTINCT_FROM) ? true :
false;
+
     // Set the left named expression to be null if the probe batch is empty.
     if (leftUpstream != IterOutcome.OK_NEW_SCHEMA && leftUpstream != IterOutcome.OK)
{
       leftExpr = null;
@@ -306,7 +311,7 @@ public class HashJoinBatch extends AbstractRecordBatch<HashJoinPOP>
{
     // Create the chained hash table
     ChainedHashTable ht =
         new ChainedHashTable(htConfig, context, oContext.getAllocator(), this.right, this.left,
null,
-            false /* nulls are not equal */);
+            areNullsEqual);
     hashTable = ht.createAndSetupHashTable(null);
   }
 

http://git-wip-us.apache.org/repos/asf/drill/blob/cafb7d4f/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/JoinUtils.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/JoinUtils.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/JoinUtils.java
new file mode 100644
index 0000000..04f3bbe
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/JoinUtils.java
@@ -0,0 +1,54 @@
+/**
+ * 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.drill.exec.physical.impl.join;
+
+import org.apache.drill.common.logical.data.JoinCondition;
+
+public class JoinUtils {
+  public static enum JoinComparator {
+    NONE, // No comparator
+    EQUALS, // Equality comparator
+    IS_NOT_DISTINCT_FROM // 'IS NOT DISTINCT FROM' comparator
+  }
+
+  // Check the comparator for the join condition. Note that a similar check is also
+  // done in JoinPrel; however we have to repeat it here because a physical plan
+  // may be submitted directly to Drill.
+  public static JoinComparator checkAndSetComparison(JoinCondition condition,
+      JoinComparator comparator) {
+    if (condition.getRelationship().equalsIgnoreCase("EQUALS") ||
+        condition.getRelationship().equals("==") /* older json plans still have '==' */)
{
+      if (comparator == JoinComparator.NONE ||
+          comparator == JoinComparator.EQUALS) {
+        return JoinComparator.EQUALS;
+      } else {
+        throw new IllegalArgumentException("This type of join does not support mixed comparators.");
+      }
+    } else if (condition.getRelationship().equalsIgnoreCase("IS_NOT_DISTINCT_FROM")) {
+      if (comparator == JoinComparator.NONE ||
+          comparator == JoinComparator.IS_NOT_DISTINCT_FROM) {
+        return JoinComparator.IS_NOT_DISTINCT_FROM;
+      } else {
+        throw new IllegalArgumentException("This type of join does not support mixed comparators.");
+      }
+    }
+    throw new IllegalArgumentException("Invalid comparator supplied to this join.");
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/drill/blob/cafb7d4f/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/MergeJoinBatch.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/MergeJoinBatch.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/MergeJoinBatch.java
index 14bc094..257b93e 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/MergeJoinBatch.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/physical/impl/join/MergeJoinBatch.java
@@ -43,6 +43,7 @@ import org.apache.drill.exec.expr.fn.FunctionGenerationHelper;
 import org.apache.drill.exec.memory.OutOfMemoryException;
 import org.apache.drill.exec.ops.FragmentContext;
 import org.apache.drill.exec.physical.config.MergeJoinPOP;
+import org.apache.drill.exec.physical.impl.join.JoinUtils.JoinComparator;
 import org.apache.drill.exec.physical.impl.join.JoinWorker.JoinOutcome;
 import org.apache.drill.exec.record.AbstractRecordBatch;
 import org.apache.drill.exec.record.BatchSchema;
@@ -111,6 +112,7 @@ public class MergeJoinBatch extends AbstractRecordBatch<MergeJoinPOP>
{
   private final JoinRelType joinType;
   private JoinWorker worker;
   public MergeJoinBatchBuilder batchBuilder;
+  private boolean areNullsEqual = false; // whether nulls compare equal
 
   protected MergeJoinBatch(MergeJoinPOP popConfig, FragmentContext context, RecordBatch left,
RecordBatch right) throws OutOfMemoryException {
     super(popConfig, context, true);
@@ -124,6 +126,13 @@ public class MergeJoinBatch extends AbstractRecordBatch<MergeJoinPOP>
{
     this.status = new JoinStatus(left, right, this);
     this.batchBuilder = new MergeJoinBatchBuilder(oContext.getAllocator(), status);
     this.conditions = popConfig.getConditions();
+
+    JoinComparator comparator = JoinComparator.NONE;
+    for (JoinCondition condition : conditions) {
+      comparator = JoinUtils.checkAndSetComparison(condition, comparator);
+    }
+    assert comparator != JoinComparator.NONE;
+    areNullsEqual = (comparator == JoinComparator.IS_NOT_DISTINCT_FROM) ? true : false;
   }
 
   public JoinRelType getJoinType() {
@@ -521,7 +530,8 @@ public class MergeJoinBatch extends AbstractRecordBatch<MergeJoinPOP>
{
 
         // If not 0, it means not equal. We return this out value.
         // Null compares to Null should returns null (unknown). In such case, we return 1
to indicate they are not equal.
-        if (compareLeftExprHolder.isOptional() && compareRightExprHolder.isOptional())
{
+        if (compareLeftExprHolder.isOptional() && compareRightExprHolder.isOptional()
+            && ! areNullsEqual) {
           JConditional jc = cg.getEvalBlock()._if(compareLeftExprHolder.getIsSet().eq(JExpr.lit(0)).
                                       cand(compareRightExprHolder.getIsSet().eq(JExpr.lit(0))));
           jc._then()._return(JExpr.lit(1));

http://git-wip-us.apache.org/repos/asf/drill/blob/cafb7d4f/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/HashJoinPrel.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/HashJoinPrel.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/HashJoinPrel.java
index d9a7277..a3c42de 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/HashJoinPrel.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/HashJoinPrel.java
@@ -113,9 +113,7 @@ public class HashJoinPrel  extends JoinPrel {
 
     List<JoinCondition> conditions = Lists.newArrayList();
 
-    for (Pair<Integer, Integer> pair : Pair.zip(leftKeys, rightKeys)) {
-      conditions.add(new JoinCondition("==", new FieldReference(leftFields.get(pair.left)),
new FieldReference(rightFields.get(pair.right))));
-    }
+    buildJoinConditions(conditions, leftFields, rightFields);
 
     HashJoinPOP hjoin = new HashJoinPOP(leftPop, rightPop, conditions, jtype);
     return creator.addMetadata(this, hjoin);

http://git-wip-us.apache.org/repos/asf/drill/blob/cafb7d4f/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/JoinPrel.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/JoinPrel.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/JoinPrel.java
index d5473f2..3541db7 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/JoinPrel.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/JoinPrel.java
@@ -21,17 +21,22 @@ package org.apache.drill.exec.planner.physical;
 import java.util.Iterator;
 import java.util.List;
 
+import org.apache.drill.common.expression.FieldReference;
+import org.apache.drill.common.logical.data.JoinCondition;
 import org.apache.drill.exec.planner.common.DrillJoinRelBase;
 import org.apache.drill.exec.planner.physical.visitor.PrelVisitor;
 import org.eigenbase.rel.InvalidRelException;
 import org.eigenbase.rel.JoinRelType;
 import org.eigenbase.rel.RelNode;
 import org.eigenbase.relopt.RelOptCluster;
+import org.eigenbase.relopt.RelOptUtil;
 import org.eigenbase.relopt.RelTraitSet;
 import org.eigenbase.reltype.RelDataType;
 import org.eigenbase.reltype.RelDataTypeField;
 import org.eigenbase.rex.RexNode;
 import org.eigenbase.rex.RexUtil;
+import org.eigenbase.sql.SqlKind;
+import org.eigenbase.util.Pair;
 
 import com.google.common.collect.Lists;
 
@@ -101,4 +106,43 @@ public abstract class JoinPrel extends DrillJoinRelBase implements Prel{
     return true;
   }
 
+  /**
+   * Build the list of join conditions for this join.
+   * A join condition is built only for equality and IS NOT DISTINCT FROM comparisons. The
difference is:
+   * null == null is FALSE whereas null IS NOT DISTINCT FROM null is TRUE
+   * For a use case of the IS NOT DISTINCT FROM comparison, see
+   * {@link org.eigenbase.rel.rules.RemoveDistinctAggregateRule}
+   * @param conditions populated list of join conditions
+   * @param leftFields join fields from the left input
+   * @param rightFields join fields from the right input
+   */
+  protected void buildJoinConditions(List<JoinCondition> conditions,
+      List<String> leftFields,
+      List<String> rightFields) {
+    List<RexNode> conjuncts = RelOptUtil.conjunctions(this.getCondition());
+    short i=0;
+
+    RexNode comp1 = null, comp2 = null;
+    for (Pair<Integer, Integer> pair : Pair.zip(leftKeys, rightKeys)) {
+      if (comp1 == null) {
+        comp1 = conjuncts.get(i++);
+        if ( ! (comp1.getKind() == SqlKind.EQUALS || comp1.getKind() == SqlKind.IS_NOT_DISTINCT_FROM))
{
+          throw new IllegalArgumentException("This type of join only supports '=' and 'is
not distinct from' comparators.");
+        }
+      } else {
+        comp2 = conjuncts.get(i++);
+        if (comp1.getKind() != comp2.getKind()) {
+          // it does not seem necessary at this time to support join conditions which have
mixed comparators - e.g
+          // 'a1 = a2 AND b1 IS NOT DISTINCT FROM b2'
+          String msg = String.format("This type of join does not support mixed comparators:
'%s' and '%s'.", comp1, comp2);
+          throw new IllegalArgumentException(msg);
+        }
+
+      }
+      conditions.add(new JoinCondition(comp1.getKind().toString(), new FieldReference(leftFields.get(pair.left)),
+          new FieldReference(rightFields.get(pair.right))));
+    }
+
+  }
+
 }

http://git-wip-us.apache.org/repos/asf/drill/blob/cafb7d4f/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/MergeJoinPrel.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/MergeJoinPrel.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/MergeJoinPrel.java
index f6b7ef6..394a82c 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/MergeJoinPrel.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/physical/MergeJoinPrel.java
@@ -99,9 +99,7 @@ public class MergeJoinPrel  extends JoinPrel {
 
     List<JoinCondition> conditions = Lists.newArrayList();
 
-    for (Pair<Integer, Integer> pair : Pair.zip(leftKeys, rightKeys)) {
-      conditions.add(new JoinCondition("==", new FieldReference(leftFields.get(pair.left)),
new FieldReference(rightFields.get(pair.right))));
-    }
+    buildJoinConditions(conditions, leftFields, rightFields);
 
     MergeJoinPOP mjoin = new MergeJoinPOP(leftPop, rightPop, conditions, jtype);
     return creator.addMetadata(this, mjoin);

http://git-wip-us.apache.org/repos/asf/drill/blob/cafb7d4f/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/TestAggregateFunctions.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/TestAggregateFunctions.java
b/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/TestAggregateFunctions.java
index 2b3ff50..6424256 100644
--- a/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/TestAggregateFunctions.java
+++ b/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/TestAggregateFunctions.java
@@ -18,6 +18,7 @@
 package org.apache.drill.exec.fn.impl;
 
 import org.apache.drill.BaseTestQuery;
+import org.apache.drill.common.types.TypeProtos;
 import org.junit.Test;
 
 public class TestAggregateFunctions extends BaseTestQuery {
@@ -56,4 +57,41 @@ public class TestAggregateFunctions extends BaseTestQuery {
         .baselineValues(0.0d)
         .go();
   }
+
+  @Test // DRILL-2092: count distinct, non distinct aggregate with group-by
+  public void testDrill2092() throws Exception {
+    String query = "select a1, b1, count(distinct c1) as dist1, \n"
+        + "sum(c1) as sum1, count(c1) as cnt1, count(*) as cnt \n"
+        + "from cp.`agg/bugs/drill2092/input.json` \n"
+        + "group by a1, b1 order by a1, b1";
+
+    String baselineQuery =
+        "select case when columns[0]='null' then cast(null as bigint) else cast(columns[0]
as bigint) end as a1, \n"
+        + "case when columns[1]='null' then cast(null as bigint) else cast(columns[1] as
bigint) end as b1, \n"
+        + "case when columns[2]='null' then cast(null as bigint) else cast(columns[2] as
bigint) end as dist1, \n"
+        + "case when columns[3]='null' then cast(null as bigint) else cast(columns[3] as
bigint) end as sum1, \n"
+        + "case when columns[4]='null' then cast(null as bigint) else cast(columns[4] as
bigint) end as cnt1, \n"
+        + "case when columns[5]='null' then cast(null as bigint) else cast(columns[5] as
bigint) end as cnt \n"
+        + "from cp.`agg/bugs/drill2092/result.tsv`";
+
+
+    // NOTE: this type of query gets rewritten by Calcite into an inner join of subqueries,
so
+    // we need to test with both hash join and merge join
+
+    testBuilder()
+        .sqlQuery(query)
+        .ordered()
+        .optionSettingQueriesForTestQuery("alter system set `planner.enable_hashjoin` = true")
+        .sqlBaselineQuery(baselineQuery)
+        .build().run();
+
+    testBuilder()
+    .sqlQuery(query)
+    .ordered()
+    .optionSettingQueriesForTestQuery("alter system set `planner.enable_hashjoin` = false")
+    .sqlBaselineQuery(baselineQuery)
+    .build().run();
+
+  }
+
 }

http://git-wip-us.apache.org/repos/asf/drill/blob/cafb7d4f/exec/java-exec/src/test/resources/agg/bugs/drill2092/input.json
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/test/resources/agg/bugs/drill2092/input.json b/exec/java-exec/src/test/resources/agg/bugs/drill2092/input.json
new file mode 100644
index 0000000..649c28d
--- /dev/null
+++ b/exec/java-exec/src/test/resources/agg/bugs/drill2092/input.json
@@ -0,0 +1,15 @@
+{ "a1" : null , "b1": null, "c1": 40}
+{ "a1" : 1 , "b1" : 1, "c1": 10 }
+{ "a1" : 1 , "b1" : 1, "c1": 20 }
+{ "a1" : 1 , "b1" : 1, "c1": 20 }
+{ "a1" : 1 , "b1" : 2, "c1": 10 }
+{ "a1" : null , "b1": null, "c1": 50}
+{ "a1" : 2 , "b1" : 2, "c1": 20 }
+{ "a1" : 2 , "b1" : 2, "c1": 30 }
+{ "a1" : 2 , "b1" : 2, "c1": 40 }
+{ "a1" : 3 , "b1" : 3, "c1": 30 }
+{ "a1" : 3 , "b1" : 3, "c1": 30 }
+{ "a1" : null , "b1": null, "c1": null}
+{ "a1" : null , "b1": null, "c1": null}
+{ "a1" : 4 , "b1": null, "c1": null}
+{ "a1" : null , "b1":4, "c1": null}

http://git-wip-us.apache.org/repos/asf/drill/blob/cafb7d4f/exec/java-exec/src/test/resources/agg/bugs/drill2092/result.tsv
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/test/resources/agg/bugs/drill2092/result.tsv b/exec/java-exec/src/test/resources/agg/bugs/drill2092/result.tsv
new file mode 100644
index 0000000..c93da13
--- /dev/null
+++ b/exec/java-exec/src/test/resources/agg/bugs/drill2092/result.tsv
@@ -0,0 +1,7 @@
+1	1	2	50	3	3
+1	2	1	10	1	1
+2	2	3	90	3	3
+3	3	1	60	2	2
+4	null	0	null	0	1
+null	4	0	null	0	1
+null	null	2	90	2	4


Mime
View raw message