org.apache.pig.piggybank.storage.HiveColumnarLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.pig.piggybank.storage.HiveColumnarLoader.java

Source

/**
 * 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.pig.piggybank.storage;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.ql.session.SessionState;
import org.apache.hadoop.hive.serde.Constants;
import org.apache.hadoop.hive.serde2.SerDeException;
import org.apache.hadoop.hive.serde2.columnar.BytesRefArrayWritable;
import org.apache.hadoop.hive.serde2.columnar.ColumnarSerDe;
import org.apache.hadoop.hive.serde2.columnar.ColumnarStruct;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.mapreduce.InputFormat;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.pig.Expression;
import org.apache.pig.FileInputLoadFunc;
import org.apache.pig.LoadMetadata;
import org.apache.pig.LoadPushDown;
import org.apache.pig.ResourceSchema;
import org.apache.pig.ResourceSchema.ResourceFieldSchema;
import org.apache.pig.ResourceStatistics;
import org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.PigSplit;
import org.apache.pig.data.DataType;
import org.apache.pig.data.Tuple;
import org.apache.pig.data.TupleFactory;
import org.apache.pig.impl.logicalLayer.FrontendException;
import org.apache.pig.impl.logicalLayer.schema.Schema;
import org.apache.pig.impl.logicalLayer.schema.Schema.FieldSchema;
import org.apache.pig.impl.util.UDFContext;
import org.apache.pig.piggybank.storage.hiverc.HiveRCInputFormat;
import org.apache.pig.piggybank.storage.hiverc.HiveRCRecordReader;
import org.apache.pig.piggybank.storage.hiverc.HiveRCSchemaUtil;
import org.apache.pig.piggybank.storage.partition.PathPartitionHelper;

/**
 * Loader for Hive RC Columnar files.<br/>
 * Supports the following types:<br/>
 * *
 * <table>
 * <tr>
 * <th>Hive Type</th>
 * <th>Pig Type from DataType</th>
 * </tr>
 * <tr>
 * <td>string</td>
 * <td>CHARARRAY</td>
 * </tr>
 * <tr>
 * <td>int</td>
 * <td>INTEGER</td>
 * </tr>
 * <tr>
 * <td>bigint or long</td>
 * <td>LONG</td>
 * </tr>
 * <tr>
 * <td>float</td>
 * <td>float</td>
 * </tr>
 * <tr>
 * <td>double</td>
 * <td>DOUBLE</td>
 * </tr>
 * <tr>
 * <td>boolean</td>
 * <td>BOOLEAN</td>
 * </tr>
 * <tr>
 * <td>byte</td>
 * <td>BYTE</td>
 * </tr>
 * <tr>
 * <td>array</td>
 * <td>TUPLE</td>
 * </tr>
 * <tr>
 * <td>map</td>
 * <td>MAP</td>
 * </tr>
 * </table>
 *
 * <p/>
 * <b>Partitions</b><br/>
 * The input paths are scanned by the loader for [partition name]=[value]
 * patterns in the subdirectories.<br/>
 * If detected these partitions are appended to the table schema.<br/>
 * For example if you have the directory structure:<br/>
 *
 * <pre>
 * /user/hive/warehouse/mytable
 *             /year=2010/month=02/day=01
 * </pre>
 *
 * The mytable schema is (id int,name string).<br/>
 * The final schema returned in pig will be (id:int, name:chararray,
 * year:chararray, month:chararray, day:chararray).<br/>
 * <p/>
 * Usage 1:
 * <p/>
 * To load a hive table: uid bigint, ts long, arr ARRAY<string,string>, m
 * MAP<String, String> <br/>
 * <code>
 * <pre>
 * a = LOAD 'file' USING HiveColumnarLoader("uid bigint, ts long, arr array<string,string>, m map<string,string>");
 * -- to reference the fields
 * b = FOREACH GENERATE a.uid, a.ts, a.arr, a.m;
 * </pre>
 * </code>
 * <p/>
 * Usage 2:
 * <p/>
 * To load a hive table: uid bigint, ts long, arr ARRAY<string,string>, m
 * MAP<String, String> only processing dates 2009-10-01 to 2009-10-02 in a <br/>
 * date partitioned hive table.<br/>
 * <b>Old Usage</b><br/>
 * <b>Note:</b> The partitions can be filtered by using pig's FILTER operator.<br/>
 * <code>
 * <pre>
 * a = LOAD 'file' USING HiveColumnarLoader("uid bigint, ts long, arr array<string,string>, m map<string,string>", "2009-10-01:2009-10-02");
 * -- to reference the fields
 * b = FOREACH GENERATE a.uid, a.ts, a.arr, a.m;
 * </pre>
 * </code> <br/>
 * <b>New Usage</b/><br/>
 * <code>
 * <pre>
 * a = LOAD 'file' USING HiveColumnarLoader("uid bigint, ts long, arr array<string,string>, m map<string,string>");
 * f = FILTER a BY daydate>='2009-10-01' AND daydate >='2009-10-02';
 * </pre>
 * </code>
 * <p/>
 * Usage 3:
 * <p/>
 * To load a hive table: uid bigint, ts long, arr ARRAY<string,string>, m
 * MAP<String, String> only reading column uid and ts for dates 2009-10-01 to
 * 2009-10-02.<br/ <br/>
 * <b>Old Usage</b><br/>
 * <b>Note:<b/> This behaviour is now supported in pig by LoadPushDown adding
 * the columns needed to be loaded like below is ignored and pig will
 * automatically send the columns used by the script to the loader.<br/>
 * <code>
 * <pre>
 * a = LOAD 'file' USING HiveColumnarLoader("uid bigint, ts long, arr array<string,string>, m map<string,string>");
 * f = FILTER a BY daydate>='2009-10-01' AND daydate >='2009-10-02';
 * -- to reference the fields
 * b = FOREACH a GENERATE uid, ts, arr, m;
 * </pre>
 * </code>
 * <p/>
 * <b>Issues</b>
 * <p/>
 * <u>Table schema definition</u><br/>
 * The schema definition must be column name followed by a space then a comma
 * then no space and the next column name and so on.<br/>
 * This so column1 string, column2 string will not work, it must be column1
 * string,column2 string
 * <p/>
 * <u>Partitioning</u><br/>
 * Partitions must be in the format [partition name]=[partition value]<br/>
 * Only strings are supported in the partitioning.<br/>
 * Partitions must follow the same naming convention for all sub directories in
 * a table<br/>
 * For example:<br/>
 * The following is not valid:<br/>
 *
 * <pre>
 *     mytable/hour=00
 *     mytable/day=01/hour=00
 * </pre>
 *
 **/
public class HiveColumnarLoader extends FileInputLoadFunc implements LoadMetadata, LoadPushDown {

    public static final String PROJECTION_ID = HiveColumnarLoader.class.getName() + ".projection";

    public static final String DATE_RANGE = HiveColumnarLoader.class.getName() + ".date-range";

    /**
     * Regex to filter out column names
     */
    protected static final Pattern pcols = Pattern.compile("[a-zA-Z_0-9]*[ ]");
    protected static final Log LOG = LogFactory.getLog(HiveColumnarLoader.class);

    protected TupleFactory tupleFactory = TupleFactory.getInstance();

    String signature = "";

    // we need to save the dateRange from the constructor if provided to add to
    // the UDFContext only when the signature is available.
    String dateRange = null;

    HiveRCRecordReader reader;

    ColumnarSerDe serde = null;
    Configuration conf = null;

    ResourceSchema pigSchema;
    boolean partitionKeysSet = false;

    BytesRefArrayWritable buff = null;

    private Properties props;
    private HiveConf hiveConf;

    transient int[] requiredColumns;

    transient Set<String> partitionColumns;

    /**
     * Implements the logic for searching partition keys and applying parition
     * filtering
     */
    transient PathPartitionHelper pathPartitionerHelper = new PathPartitionHelper();

    transient Path currentPath = null;
    transient Map<String, String> currentPathPartitionKeyMap;

    /**
     * Table schema should be a space and comma separated string describing the
     * Hive schema.<br/>
     * For example uid BIGINT, pid long, means 1 column of uid type BIGINT and
     * one column of pid type LONG.<br/>
     * The types are not case sensitive.
     *
     * @param table_schema
     *            This property cannot be null
     */
    public HiveColumnarLoader(String table_schema) {
        setup(table_schema);
    }

    /**
     * This constructor is for backward compatibility.
     *
     * Table schema should be a space and comma separated string describing the
     * Hive schema.<br/>
     * For example uid BIGINT, pid long, means 1 column of uid type BIGINT and
     * one column of pid type LONG.<br/>
     * The types are not case sensitive.
     *
     * @param table_schema
     *            This property cannot be null
     * @param dateRange
     *            String
     * @param columns
     *            String not used any more
     */
    public HiveColumnarLoader(String table_schema, String dateRange, String columns) {
        setup(table_schema);

        this.dateRange = dateRange;
    }

    /**
     * This constructor is for backward compatibility.
     *
     * Table schema should be a space and comma separated string describing the
     * Hive schema.<br/>
     * For example uid BIGINT, pid long, means 1 column of uid type BIGINT and
     * one column of pid type LONG.<br/>
     * The types are not case sensitive.
     *
     * @param table_schema
     *            This property cannot be null
     * @param dateRange
     *            String
     */
    public HiveColumnarLoader(String table_schema, String dateRange) {
        setup(table_schema);

        this.dateRange = dateRange;
    }

    private Properties getUDFContext() {
        return UDFContext.getUDFContext().getUDFProperties(this.getClass(), new String[] { signature });
    }

    @Override
    public InputFormat<LongWritable, BytesRefArrayWritable> getInputFormat() throws IOException {
        LOG.info("Signature: " + signature);
        return new HiveRCInputFormat(signature);
    }

    @Override
    public Tuple getNext() throws IOException {
        Tuple tuple = null;

        try {
            if (reader.nextKeyValue()) {

                BytesRefArrayWritable buff = reader.getCurrentValue();
                ColumnarStruct struct = readColumnarStruct(buff);

                tuple = readColumnarTuple(struct, reader.getSplitPath());
            }

        } catch (InterruptedException e) {
            throw new IOException(e.toString(), e);
        }

        return tuple;
    }

    @Override
    public void prepareToRead(@SuppressWarnings("rawtypes") RecordReader reader, PigSplit split)
            throws IOException {

        this.reader = (HiveRCRecordReader) reader;

        // check that the required indexes actually exist i.e. the columns that
        // should be read.
        // assuming this is always defined simplifies the readColumnarTuple
        // logic.

        int requiredIndexes[] = getRequiredColumns();
        if (requiredIndexes == null) {

            int fieldLen = pigSchema.getFields().length;

            // if any the partition keys should already exist
            String[] partitionKeys = getPartitionKeys(null, null);
            if (partitionKeys != null) {
                fieldLen += partitionKeys.length;
            }

            requiredIndexes = new int[fieldLen];

            for (int i = 0; i < fieldLen; i++) {
                requiredIndexes[i] = i;
            }

            this.requiredColumns = requiredIndexes;
        }

        try {
            serde = new ColumnarSerDe();
            serde.initialize(hiveConf, props);
        } catch (SerDeException e) {
            LOG.error(e.toString(), e);
            throw new IOException(e);
        }

    }

    @Override
    public void setLocation(String location, Job job) throws IOException {
        FileInputFormat.setInputPaths(job, location);
    }

    /**
     * Does the configuration setup and schema parsing and setup.
     *
     * @param table_schema
     *            String
     * @param columnsToRead
     *            String
     */
    private void setup(String table_schema) {

        if (table_schema == null)
            throw new RuntimeException(
                    "The table schema must be defined as colname type, colname type.  All types are hive types");

        // create basic configuration for hdfs and hive
        conf = new Configuration();
        hiveConf = new HiveConf(conf, SessionState.class);

        // parse the table_schema string
        List<String> types = HiveRCSchemaUtil.parseSchemaTypes(table_schema);
        List<String> cols = HiveRCSchemaUtil.parseSchema(pcols, table_schema);

        List<FieldSchema> fieldSchemaList = new ArrayList<FieldSchema>(cols.size());

        for (int i = 0; i < cols.size(); i++) {
            fieldSchemaList.add(new FieldSchema(cols.get(i), HiveRCSchemaUtil.findPigDataType(types.get(i))));
        }

        pigSchema = new ResourceSchema(new Schema(fieldSchemaList));

        props = new Properties();

        // setting table schema properties for ColumnarSerDe
        // these properties are never changed by the columns to read filter,
        // because the columnar serde needs to now the
        // complete format of each record.
        props.setProperty(Constants.LIST_COLUMNS, HiveRCSchemaUtil.listToString(cols));
        props.setProperty(Constants.LIST_COLUMN_TYPES, HiveRCSchemaUtil.listToString(types));

    }

    /**
     * Uses the ColumnarSerde to deserialize the buff:BytesRefArrayWritable into
     * a ColumnarStruct instance.
     *
     * @param buff
     *            BytesRefArrayWritable
     * @return ColumnarStruct
     */
    private ColumnarStruct readColumnarStruct(BytesRefArrayWritable buff) {
        // use ColumnarSerDe to deserialize row
        ColumnarStruct struct = null;
        try {
            struct = (ColumnarStruct) serde.deserialize(buff);
        } catch (SerDeException e) {
            LOG.error(e.toString(), e);
            throw new RuntimeException(e.toString(), e);
        }

        return struct;
    }

    /**
     * Only read the columns that were requested in the constructor.<br/>
     *
     * @param struct
     *            ColumnarStruct
     * @param path
     *            Path
     * @return Tuple
     * @throws IOException
     */
    private Tuple readColumnarTuple(ColumnarStruct struct, Path path) throws IOException {

        int[] columnIndexes = getRequiredColumns();
        // the partition keys if any will already be in the UDFContext here.
        String[] partitionKeys = getPartitionKeys(null, null);
        // only if the path has changed should be run the
        if (currentPath == null || !currentPath.equals(path)) {
            currentPathPartitionKeyMap = (partitionKeys == null) ? null
                    : pathPartitionerHelper.getPathPartitionKeyValues(path.toString());
            currentPath = path;
        }

        // if the partitionColumns is null this value will stop the for loop
        // below from trynig to add any partition columns
        // that do not exist
        int partitionColumnStartIndex = Integer.MAX_VALUE;

        if (!(partitionColumns == null || partitionColumns.size() == 0)) {
            // partition columns are always appended to the schema fields.
            partitionColumnStartIndex = pigSchema.getFields().length;

        }

        // create tuple with determined previous size
        Tuple t = tupleFactory.newTuple(columnIndexes.length);

        // read in all columns
        for (int i = 0; i < columnIndexes.length; i++) {
            int columnIndex = columnIndexes[i];

            if (columnIndex < partitionColumnStartIndex) {
                Object obj = struct.getField(columnIndex);
                Object pigType = HiveRCSchemaUtil.extractPigTypeFromHiveType(obj);

                t.set(i, pigType);

            } else {
                // read the partition columns
                // will only be executed if partitionColumns is not null
                String key = partitionKeys[columnIndex - partitionColumnStartIndex];
                Object value = currentPathPartitionKeyMap.get(key);
                t.set(i, value);

            }

        }

        return t;
    }

    /**
     * Will parse the required columns from the UDFContext properties if the
     * requiredColumns[] variable is null, or else just return the
     * requiredColumns.
     *
     * @return int[]
     */
    private int[] getRequiredColumns() {

        if (requiredColumns == null) {
            Properties properties = getUDFContext();

            String projectionStr = properties.getProperty(PROJECTION_ID);

            if (projectionStr != null) {
                String[] split = projectionStr.split(",");
                int columnIndexes[] = new int[split.length];

                int index = 0;
                for (String splitItem : split) {
                    columnIndexes[index++] = Integer.parseInt(splitItem);
                }

                requiredColumns = columnIndexes;
            }

        }

        return requiredColumns;
    }

    /**
     * Reads the partition columns
     *
     * @param location
     * @param job
     * @return
     */
    private Set<String> getPartitionColumns(String location, Job job) {

        if (partitionColumns == null) {
            // read the partition columns from the UDF Context first.
            // if not in the UDF context then read it using the PathPartitioner.

            Properties properties = getUDFContext();

            if (properties == null)
                properties = new Properties();

            String partitionColumnStr = properties.getProperty(PathPartitionHelper.PARTITION_COLUMNS);

            if (partitionColumnStr == null && !(location == null || job == null)) {
                // if it hasn't been written yet.
                Set<String> partitionColumnSet;

                try {
                    partitionColumnSet = pathPartitionerHelper.getPartitionKeys(location, job.getConfiguration());
                } catch (IOException e) {

                    RuntimeException rte = new RuntimeException(e);
                    rte.setStackTrace(e.getStackTrace());
                    throw rte;

                }

                if (partitionColumnSet != null) {

                    StringBuilder buff = new StringBuilder();

                    int i = 0;
                    for (String column : partitionColumnSet) {
                        if (i++ != 0) {
                            buff.append(',');
                        }

                        buff.append(column);
                    }

                    String buffStr = buff.toString().trim();

                    if (buffStr.length() > 0) {

                        properties.setProperty(PathPartitionHelper.PARTITION_COLUMNS, buff.toString());
                    }

                    partitionColumns = partitionColumnSet;

                }

            } else {
                // the partition columns has been set already in the UDF Context
                if (partitionColumnStr != null) {
                    String split[] = partitionColumnStr.split(",");
                    partitionColumns = new LinkedHashSet<String>();
                    if (split.length > 0) {
                        for (String splitItem : split) {
                            partitionColumns.add(splitItem);
                        }
                    }
                }

            }

        }

        return partitionColumns;

    }

    @Override
    public String[] getPartitionKeys(String location, Job job) throws IOException {
        Set<String> partitionKeys = getPartitionColumns(location, job);

        return partitionKeys == null ? null : partitionKeys.toArray(new String[] {});
    }

    @Override
    public ResourceSchema getSchema(String location, Job job) throws IOException {

        if (!partitionKeysSet) {
            Set<String> keys = getPartitionColumns(location, job);

            if (!(keys == null || keys.size() == 0)) {

                // re-edit the pigSchema to contain the new partition keys.
                ResourceFieldSchema[] fields = pigSchema.getFields();

                LOG.debug("Schema: " + Arrays.toString(fields));

                ResourceFieldSchema[] newFields = Arrays.copyOf(fields, fields.length + keys.size());

                int index = fields.length;

                for (String key : keys) {
                    newFields[index++] = new ResourceFieldSchema(new FieldSchema(key, DataType.CHARARRAY));
                }

                pigSchema.setFields(newFields);

                LOG.debug("Added partition fields: " + keys + " to loader schema");
                LOG.debug("Schema is: " + Arrays.toString(newFields));
            }

            partitionKeysSet = true;

        }

        return pigSchema;
    }

    @Override
    public ResourceStatistics getStatistics(String location, Job job) throws IOException {
        return null;
    }

    @Override
    public void setPartitionFilter(Expression partitionFilter) throws IOException {
        getUDFContext().setProperty(PathPartitionHelper.PARITITION_FILTER_EXPRESSION, partitionFilter.toString());
    }

    @Override
    public List<OperatorSet> getFeatures() {
        return Arrays.asList(LoadPushDown.OperatorSet.PROJECTION);
    }

    @Override
    public RequiredFieldResponse pushProjection(RequiredFieldList requiredFieldList) throws FrontendException {

        // save the required field list to the UDFContext properties.
        StringBuilder buff = new StringBuilder();

        int i = 0;
        for (RequiredField f : requiredFieldList.getFields()) {
            if (i++ != 0)
                buff.append(',');

            buff.append(f.getIndex());
        }

        Properties properties = getUDFContext();

        properties.setProperty(PROJECTION_ID, buff.toString());

        return new RequiredFieldResponse(true);
    }

    @Override
    public void setUDFContextSignature(String signature) {
        super.setUDFContextSignature(signature);

        LOG.debug("Signature: " + signature);
        this.signature = signature;

        // this provides backwards compatibility
        // the HiveRCInputFormat will read this and if set will perform the
        // needed partitionFiltering
        if (dateRange != null) {
            getUDFContext().setProperty(DATE_RANGE, dateRange);
        }

    }

}