You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kylin.apache.org by li...@apache.org on 2017/06/05 05:23:42 UTC

[54/67] [abbrv] kylin git commit: KYLIN-2575 translate computed column back to expression when sending to adhoc

KYLIN-2575 translate computed column back to expression when sending to adhoc


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

Branch: refs/heads/master
Commit: 7c381487069d2bcf3adb07b38dc0b0803d13a8b4
Parents: 508fc23
Author: Hongbin Ma <ma...@apache.org>
Authored: Tue May 30 19:14:46 2017 +0800
Committer: hongbin ma <ma...@kyligence.io>
Committed: Tue May 30 23:47:23 2017 +0800

----------------------------------------------------------------------
 assembly/pom.xml                                |   6 -
 .../apache/kylin/metadata/MetadataManager.java  |  12 +-
 .../kylin/metadata/model/DataModelDesc.java     |   8 +-
 .../org/apache/kylin/query/KylinTestBase.java   |  33 ++--
 pom.xml                                         |   4 +
 server-base/pom.xml                             |  12 +-
 .../apache/kylin/rest/service/QueryService.java |   2 +-
 .../org/apache/kylin/rest/util/AdHocUtil.java   | 154 ++++++++++++++++++-
 .../apache/kylin/rest/util/AdHocUtilTest.java   |  98 ++++++++++++
 9 files changed, 296 insertions(+), 33 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/kylin/blob/7c381487/assembly/pom.xml
----------------------------------------------------------------------
diff --git a/assembly/pom.xml b/assembly/pom.xml
index dae7152..0a64dde 100644
--- a/assembly/pom.xml
+++ b/assembly/pom.xml
@@ -91,12 +91,6 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.apache.mrunit</groupId>
-            <artifactId>mrunit</artifactId>
-            <classifier>hadoop2</classifier>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
             <groupId>org.apache.hbase</groupId>
             <artifactId>hbase-common</artifactId>
             <scope>provided</scope>

http://git-wip-us.apache.org/repos/asf/kylin/blob/7c381487/core-metadata/src/main/java/org/apache/kylin/metadata/MetadataManager.java
----------------------------------------------------------------------
diff --git a/core-metadata/src/main/java/org/apache/kylin/metadata/MetadataManager.java b/core-metadata/src/main/java/org/apache/kylin/metadata/MetadataManager.java
index f8e6832..2a894b9 100644
--- a/core-metadata/src/main/java/org/apache/kylin/metadata/MetadataManager.java
+++ b/core-metadata/src/main/java/org/apache/kylin/metadata/MetadataManager.java
@@ -116,13 +116,21 @@ public class MetadataManager {
     private CaseInsensitiveStringCache<ExternalFilterDesc> extFilterMap;
 
     public static class CCInfo {
-        public ComputedColumnDesc computedColumnDesc;
-        public Set<DataModelDesc> dataModelDescs;
+        private ComputedColumnDesc computedColumnDesc;
+        private Set<DataModelDesc> dataModelDescs;
 
         public CCInfo(ComputedColumnDesc computedColumnDesc, Set<DataModelDesc> dataModelDescs) {
             this.computedColumnDesc = computedColumnDesc;
             this.dataModelDescs = dataModelDescs;
         }
+
+        public ComputedColumnDesc getComputedColumnDesc() {
+            return computedColumnDesc;
+        }
+
+        public Set<DataModelDesc> getDataModelDescs() {
+            return dataModelDescs;
+        }
     }
 
     private Map<String, CCInfo> ccInfoMap = Maps.newHashMap();// this is to check any two models won't conflict computed columns

http://git-wip-us.apache.org/repos/asf/kylin/blob/7c381487/core-metadata/src/main/java/org/apache/kylin/metadata/model/DataModelDesc.java
----------------------------------------------------------------------
diff --git a/core-metadata/src/main/java/org/apache/kylin/metadata/model/DataModelDesc.java b/core-metadata/src/main/java/org/apache/kylin/metadata/model/DataModelDesc.java
index e759bdf..f5092a8 100644
--- a/core-metadata/src/main/java/org/apache/kylin/metadata/model/DataModelDesc.java
+++ b/core-metadata/src/main/java/org/apache/kylin/metadata/model/DataModelDesc.java
@@ -476,13 +476,13 @@ public class DataModelDesc extends RootPersistentEntity {
             }
 
             CCInfo other = ccInfoMap.get(computedColumnDesc.getFullName());
-            if (other == null || (other.dataModelDescs.size() == 1 && other.dataModelDescs.contains(this))) {
+            if (other == null || (other.getDataModelDescs().size() == 1 && other.getDataModelDescs().contains(this))) {
                 ccInfoMap.put(computedColumnDesc.getFullName(), new CCInfo(computedColumnDesc, Sets.<DataModelDesc> newHashSet(this)));
-            } else if (other.computedColumnDesc.equals(computedColumnDesc)) {
-                other.dataModelDescs.add(this);
+            } else if (other.getComputedColumnDesc().equals(computedColumnDesc)) {
+                other.getDataModelDescs().add(this);
             } else {
                 throw new IllegalStateException(String.format("Computed column named %s is already defined in other models: %s. Please change another name, or try to keep consistent definition", //
-                        computedColumnDesc.getFullName(), other.dataModelDescs));
+                        computedColumnDesc.getFullName(), other.getDataModelDescs()));
             }
         }
     }

http://git-wip-us.apache.org/repos/asf/kylin/blob/7c381487/kylin-it/src/test/java/org/apache/kylin/query/KylinTestBase.java
----------------------------------------------------------------------
diff --git a/kylin-it/src/test/java/org/apache/kylin/query/KylinTestBase.java b/kylin-it/src/test/java/org/apache/kylin/query/KylinTestBase.java
index 42f3a44..0db5388 100644
--- a/kylin-it/src/test/java/org/apache/kylin/query/KylinTestBase.java
+++ b/kylin-it/src/test/java/org/apache/kylin/query/KylinTestBase.java
@@ -42,15 +42,14 @@ import java.util.Set;
 import java.util.TreeSet;
 import java.util.logging.LogManager;
 
-import com.google.common.collect.Lists;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.common.util.HBaseMetadataTestCase;
 import org.apache.kylin.common.util.Pair;
 import org.apache.kylin.metadata.project.ProjectInstance;
+import org.apache.kylin.metadata.querymeta.SelectedColumnMeta;
 import org.apache.kylin.query.relnode.OLAPContext;
 import org.apache.kylin.query.routing.rules.RemoveBlackoutRealizationsRule;
-import org.apache.kylin.metadata.querymeta.SelectedColumnMeta;
 import org.apache.kylin.rest.util.AdHocUtil;
 import org.dbunit.DatabaseUnitException;
 import org.dbunit.database.DatabaseConfig;
@@ -70,6 +69,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 import com.google.common.io.Files;
 
 /**
@@ -110,7 +110,8 @@ public class KylinTestBase {
     // h2 (BIGINT)
     public static class TestH2DataTypeFactory extends H2DataTypeFactory {
         @Override
-        public DataType createDataType(int sqlType, String sqlTypeName, String tableName, String columnName) throws DataTypeException {
+        public DataType createDataType(int sqlType, String sqlTypeName, String tableName, String columnName)
+                throws DataTypeException {
 
             if ((columnName.startsWith("COL") || columnName.startsWith("col")) && sqlType == Types.BIGINT) {
                 return DataType.INTEGER;
@@ -223,7 +224,8 @@ public class KylinTestBase {
     // ////////////////////////////////////////////////////////////////////////////////////////
     // execute
 
-    protected ITable executeQuery(IDatabaseConnection dbConn, String queryName, String sql, boolean needSort) throws Exception {
+    protected ITable executeQuery(IDatabaseConnection dbConn, String queryName, String sql, boolean needSort)
+            throws Exception {
 
         // change join type to match current setting
         sql = changeJoinType(sql, joinType);
@@ -258,9 +260,9 @@ public class KylinTestBase {
 
             return output(resultSet, needDisplay);
         } catch (SQLException sqlException) {
-            List<List<String>> results =  Lists.newArrayList();
+            List<List<String>> results = Lists.newArrayList();
             List<SelectedColumnMeta> columnMetas = Lists.newArrayList();
-            AdHocUtil.doAdHocQuery(sql, results, columnMetas, sqlException);
+            AdHocUtil.doAdHocQuery(ProjectInstance.DEFAULT_PROJECT_NAME, sql, results, columnMetas, sqlException);
             return results.size();
         } finally {
             if (resultSet != null) {
@@ -280,7 +282,8 @@ public class KylinTestBase {
         }
     }
 
-    protected ITable executeDynamicQuery(IDatabaseConnection dbConn, String queryName, String sql, List<String> parameters, boolean needSort) throws Exception {
+    protected ITable executeDynamicQuery(IDatabaseConnection dbConn, String queryName, String sql,
+            List<String> parameters, boolean needSort) throws Exception {
 
         // change join type to match current setting
         sql = changeJoinType(sql, joinType);
@@ -316,7 +319,8 @@ public class KylinTestBase {
 
         String[] tokens = StringUtils.split(sql, null);// split white spaces
         for (int i = 0; i < tokens.length - 1; ++i) {
-            if ((tokens[i].equalsIgnoreCase("inner") || tokens[i].equalsIgnoreCase("left")) && tokens[i + 1].equalsIgnoreCase("join")) {
+            if ((tokens[i].equalsIgnoreCase("inner") || tokens[i].equalsIgnoreCase("left"))
+                    && tokens[i + 1].equalsIgnoreCase("join")) {
                 tokens[i] = targetType.toLowerCase();
             }
         }
@@ -407,7 +411,8 @@ public class KylinTestBase {
         }
     }
 
-    protected void execAndCompResultSize(String queryFolder, String[] exclusiveQuerys, boolean needSort) throws Exception {
+    protected void execAndCompResultSize(String queryFolder, String[] exclusiveQuerys, boolean needSort)
+            throws Exception {
         logger.info("---------- test folder: " + queryFolder);
         Set<String> exclusiveSet = buildExclusiveSet(exclusiveQuerys);
 
@@ -504,7 +509,6 @@ public class KylinTestBase {
         logger.info("Queries appended with limit: " + appendLimitQueries);
     }
 
-
     protected void execAndCompQuery(String queryFolder, String[] exclusiveQuerys, boolean needSort) throws Exception {
         execAndCompQuery(queryFolder, exclusiveQuerys, needSort, new ICompareQueryTranslator() {
             @Override
@@ -518,7 +522,8 @@ public class KylinTestBase {
         });
     }
 
-    protected void execAndCompQuery(String queryFolder, String[] exclusiveQuerys, boolean needSort, ICompareQueryTranslator translator) throws Exception {
+    protected void execAndCompQuery(String queryFolder, String[] exclusiveQuerys, boolean needSort,
+            ICompareQueryTranslator translator) throws Exception {
         logger.info("---------- test folder: " + new File(queryFolder).getAbsolutePath());
         Set<String> exclusiveSet = buildExclusiveSet(exclusiveQuerys);
 
@@ -557,7 +562,8 @@ public class KylinTestBase {
         }
     }
 
-    protected void execAndCompDynamicQuery(String queryFolder, String[] exclusiveQuerys, boolean needSort) throws Exception {
+    protected void execAndCompDynamicQuery(String queryFolder, String[] exclusiveQuerys, boolean needSort)
+            throws Exception {
         logger.info("---------- test folder: " + queryFolder);
         Set<String> exclusiveSet = buildExclusiveSet(exclusiveQuerys);
 
@@ -683,7 +689,8 @@ public class KylinTestBase {
         cubeConnection = QueryDataSource.create(ProjectInstance.DEFAULT_PROJECT_NAME, config).getConnection();
 
         //setup h2
-        h2Connection = DriverManager.getConnection("jdbc:h2:mem:db" + (h2InstanceCount++) + ";CACHE_SIZE=32072", "sa", "");
+        h2Connection = DriverManager.getConnection("jdbc:h2:mem:db" + (h2InstanceCount++) + ";CACHE_SIZE=32072", "sa",
+                "");
         // Load H2 Tables (inner join)
         H2Database h2DB = new H2Database(h2Connection, config);
         h2DB.loadAllTables();

http://git-wip-us.apache.org/repos/asf/kylin/blob/7c381487/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 2fcc6fa..f887c8d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -93,6 +93,8 @@
         <h2.version>1.4.192</h2.version>
         <jetty.version>9.2.20.v20161216</jetty.version>
         <jamm.version>0.3.1</jamm.version>
+        <mockito.version>2.7.14</mockito.version>
+
 
         <!-- Commons -->
         <commons-lang3.version>3.4</commons-lang3.version>
@@ -730,6 +732,8 @@
                 <artifactId>opensaml</artifactId>
                 <version>${opensaml.version}</version>
             </dependency>
+
+
             <!-- Spring Core -->
             <dependency>
                 <groupId>org.springframework</groupId>

http://git-wip-us.apache.org/repos/asf/kylin/blob/7c381487/server-base/pom.xml
----------------------------------------------------------------------
diff --git a/server-base/pom.xml b/server-base/pom.xml
index b165b99..c7247a5 100644
--- a/server-base/pom.xml
+++ b/server-base/pom.xml
@@ -17,7 +17,8 @@
  limitations under the License.
 -->
 
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
@@ -47,7 +48,7 @@
                 </exclusion>
             </exclusions>
         </dependency>
-        
+
         <!-- these plug-in modules, should not have API dependencies -->
         <dependency>
             <groupId>org.apache.kylin</groupId>
@@ -134,6 +135,13 @@
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+            <!--MRUnit relies on older version of mockito, so cannot manage it globally-->
+            <version>${mockito.version}</version>
+        </dependency>
 
         <dependency>
             <groupId>org.apache.tomcat</groupId>

http://git-wip-us.apache.org/repos/asf/kylin/blob/7c381487/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java
----------------------------------------------------------------------
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java b/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java
index 5130e55..06f9d80 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java
@@ -729,7 +729,7 @@ public class QueryService extends BasicService {
                 results.add(oneRow);
             }
         } catch (SQLException sqlException) {
-            isAdHoc = AdHocUtil.doAdHocQuery(correctedSql, results, columnMetas, sqlException);
+            isAdHoc = AdHocUtil.doAdHocQuery(sqlRequest.getProject(), correctedSql, results, columnMetas, sqlException);
         } finally {
             close(resultSet, stat, conn);
         }

http://git-wip-us.apache.org/repos/asf/kylin/blob/7c381487/server-base/src/main/java/org/apache/kylin/rest/util/AdHocUtil.java
----------------------------------------------------------------------
diff --git a/server-base/src/main/java/org/apache/kylin/rest/util/AdHocUtil.java b/server-base/src/main/java/org/apache/kylin/rest/util/AdHocUtil.java
index 678e58e..8221790 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/util/AdHocUtil.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/util/AdHocUtil.java
@@ -18,11 +18,25 @@
 
 package org.apache.kylin.rest.util;
 
+import static org.apache.kylin.metadata.MetadataManager.CCInfo;
+
 import java.sql.SQLException;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
 
+import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.commons.lang3.tuple.Triple;
 import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.metadata.MetadataManager;
+import org.apache.kylin.metadata.model.DataModelDesc;
+import org.apache.kylin.metadata.project.ProjectInstance;
+import org.apache.kylin.metadata.project.ProjectManager;
 import org.apache.kylin.metadata.querymeta.SelectedColumnMeta;
 import org.apache.kylin.query.routing.NoRealizationFoundException;
 import org.apache.kylin.rest.exception.InternalErrorException;
@@ -31,11 +45,19 @@ import org.apache.kylin.storage.adhoc.IAdhocConverter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
 public class AdHocUtil {
     private static final Logger logger = LoggerFactory.getLogger(AdHocUtil.class);
 
-    public static boolean doAdHocQuery(String sql, List<List<String>> results, List<SelectedColumnMeta> columnMetas, SQLException sqlException) throws Exception {
-        boolean isExpectedCause = (ExceptionUtils.getRootCause(sqlException).getClass().equals(NoRealizationFoundException.class));
+    public static boolean doAdHocQuery(String project, String sql, List<List<String>> results,
+            List<SelectedColumnMeta> columnMetas, SQLException sqlException) throws Exception {
+
+        boolean isExpectedCause = (ExceptionUtils.getRootCause(sqlException).getClass()
+                .equals(NoRealizationFoundException.class));
         KylinConfig kylinConfig = KylinConfig.getInstanceFromEnv();
         Boolean isAdHoc = false;
 
@@ -62,9 +84,10 @@ public class AdHocUtil {
             runner.init();
 
             try {
-                String adhocSql = converter.convert(sql);
-                if (!sql.equals(adhocSql)) {
-                    logger.info("the original query is converted to {} before delegating to adhoc", adhocSql);
+                String expandCC = restoreComputedColumnToExpr(sql, project);
+                String adhocSql = converter.convert(expandCC);
+                if (!adhocSql.equals(adhocSql)) {
+                    logger.info("before delegating to adhoc, the query is converted to {} ", adhocSql);
                 }
 
                 runner.executeQuery(adhocSql, results, columnMetas);
@@ -78,4 +101,125 @@ public class AdHocUtil {
 
         return isAdHoc;
     }
+
+    private final static Pattern identifierInSqlPattern = Pattern.compile(
+            //find pattern like "table"."column" or "column"
+            "((?<![\\p{L}_0-9\\.\\\"])(\\\"[\\p{L}_0-9]+\\\"\\.)?(\\\"[\\p{L}_0-9]+\\\")(?![\\p{L}_0-9\\.\\\"]))" + "|"
+            //find pattern like table.column or column
+                    + "((?<![\\p{L}_0-9\\.\\\"])([\\p{L}_0-9]+\\.)?([\\p{L}_0-9]+)(?![\\p{L}_0-9\\.\\\"]))");
+
+    private final static Pattern identifierInExprPattern = Pattern.compile(
+            // a.b.c
+            "((?<![\\p{L}_0-9\\.\\\"])([\\p{L}_0-9]+\\.)([\\p{L}_0-9]+\\.)([\\p{L}_0-9]+)(?![\\p{L}_0-9\\.\\\"]))");
+
+    private final static Pattern endWithAsPattern = Pattern.compile("\\s+as\\s+$", Pattern.CASE_INSENSITIVE);
+
+    public static String restoreComputedColumnToExpr(String beforeSql, String project) {
+        MetadataManager metadataManager = MetadataManager.getInstance(KylinConfig.getInstanceFromEnv());
+        Map<String, CCInfo> ccInfoMap = metadataManager.getCcInfoMap();
+        final ProjectInstance projectInstance = ProjectManager.getInstance(KylinConfig.getInstanceFromEnv())
+                .getProject(project);
+
+        Iterable<CCInfo> projectCCInfo = Iterables.filter(ccInfoMap.values(), new Predicate<CCInfo>() {
+            @Override
+            public boolean apply(@Nullable CCInfo ccInfo) {
+                return Iterables.any(ccInfo.getDataModelDescs(), new Predicate<DataModelDesc>() {
+                    @Override
+                    public boolean apply(@Nullable DataModelDesc model) {
+                        return projectInstance.containsModel(model.getName());
+                    }
+                });
+            }
+        });
+
+        String afterSql = beforeSql;
+        for (CCInfo ccInfo : projectCCInfo) {
+            afterSql = restoreComputedColumnToExpr(afterSql, ccInfo);
+        }
+
+        if (!StringUtils.equals(beforeSql, afterSql)) {
+            logger.info("computed column in sql is expanded before sending to adhoc engine: " + afterSql);
+        }
+        return afterSql;
+    }
+
+    static String restoreComputedColumnToExpr(String sql, CCInfo ccInfo) {
+
+        String ccName = ccInfo.getComputedColumnDesc().getColumnName();
+        List<Triple<Integer, Integer, String>> replacements = Lists.newArrayList();
+        Matcher matcher = identifierInSqlPattern.matcher(sql);
+
+        while (matcher.find()) {
+            if (matcher.group(1) != null) { //with quote case: "TABLE"."COLUMN"
+
+                String quotedColumnName = matcher.group(3);
+                Preconditions.checkNotNull(quotedColumnName);
+                String columnName = StringUtils.strip(quotedColumnName, "\"");
+                if (!columnName.equalsIgnoreCase(ccName)) {
+                    continue;
+                }
+
+                if (matcher.group(2) != null) { // table name exist 
+                    String quotedTableAlias = StringUtils.strip(matcher.group(2), ".");
+                    String tableAlias = StringUtils.strip(quotedTableAlias, "\"");
+                    replacements.add(Triple.of(matcher.start(1), matcher.end(1),
+                            replaceIdentifierInExpr(ccInfo.getComputedColumnDesc().getExpression(), tableAlias, true)));
+                } else { //only column
+                    if (endWithAsPattern.matcher(sql.substring(0, matcher.start(1))).find()) {
+                        //select DEAL_AMOUNT as "deal_amount" case
+                        continue;
+                    }
+                    replacements.add(Triple.of(matcher.start(1), matcher.end(1),
+                            replaceIdentifierInExpr(ccInfo.getComputedColumnDesc().getExpression(), null, true)));
+                }
+            } else if (matcher.group(4) != null) { //without quote case: table.column or simply column
+                String columnName = matcher.group(6);
+                Preconditions.checkNotNull(columnName);
+                if (!columnName.equalsIgnoreCase(ccName)) {
+                    continue;
+                }
+
+                if (matcher.group(5) != null) { //table name exist
+                    String tableAlias = StringUtils.strip(matcher.group(5), ".");
+                    replacements.add(Triple.of(matcher.start(4), matcher.end(4), replaceIdentifierInExpr(
+                            ccInfo.getComputedColumnDesc().getExpression(), tableAlias, false)));
+
+                } else { //only column 
+                    if (endWithAsPattern.matcher(sql.substring(0, matcher.start(4))).find()) {
+                        //select DEAL_AMOUNT as deal_amount case
+                        continue;
+                    }
+                    replacements.add(Triple.of(matcher.start(4), matcher.end(4),
+                            replaceIdentifierInExpr(ccInfo.getComputedColumnDesc().getExpression(), null, false)));
+                }
+            }
+        }
+
+        Collections.reverse(replacements);
+        for (Triple<Integer, Integer, String> triple : replacements) {
+            sql = sql.substring(0, triple.getLeft()) + "(" + triple.getRight() + ")"
+                    + sql.substring(triple.getMiddle());
+        }
+        return sql;
+    }
+
+    // identifier in expr must be DB.TABLE.COLUMN, all TABLE in expr should be guaranteed to be same
+    static String replaceIdentifierInExpr(String expr, String tableAlias, boolean quoted) {
+        List<Triple<Integer, Integer, String>> replacements = Lists.newArrayList();
+        Matcher matcher = identifierInExprPattern.matcher(expr);
+        while (matcher.find()) {
+
+            String t = tableAlias == null ? StringUtils.strip(matcher.group(3), ".") : tableAlias;
+            String c = matcher.group(4);
+
+            String replacement = quoted ? "\"" + t.toUpperCase() + "\".\"" + c.toUpperCase() + "\"" : t + "." + c;
+            replacements.add(Triple.of(matcher.start(1), matcher.end(1), replacement));
+        }
+
+        Collections.reverse(replacements);
+        for (Triple<Integer, Integer, String> triple : replacements) {
+            expr = expr.substring(0, triple.getLeft()) + triple.getRight() + expr.substring(triple.getMiddle());
+        }
+        return expr;
+    }
 }

http://git-wip-us.apache.org/repos/asf/kylin/blob/7c381487/server-base/src/test/java/org/apache/kylin/rest/util/AdHocUtilTest.java
----------------------------------------------------------------------
diff --git a/server-base/src/test/java/org/apache/kylin/rest/util/AdHocUtilTest.java b/server-base/src/test/java/org/apache/kylin/rest/util/AdHocUtilTest.java
new file mode 100644
index 0000000..b93e2d3
--- /dev/null
+++ b/server-base/src/test/java/org/apache/kylin/rest/util/AdHocUtilTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.kylin.rest.util;
+
+import static org.apache.kylin.metadata.MetadataManager.CCInfo;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.kylin.metadata.model.ComputedColumnDesc;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AdHocUtilTest {
+
+    @Test
+    public void testReplaceIdentifierInExpr() {
+        {
+            String ret = AdHocUtil.replaceIdentifierInExpr("a.b.x * a.b.y", null, false);
+            Assert.assertEquals("b.x * b.y", ret);
+        }
+        {
+            String ret = AdHocUtil.replaceIdentifierInExpr("a_1.b_2.x_3 * a_1.b_2.y_3", null, false);
+            Assert.assertEquals("b_2.x_3 * b_2.y_3", ret);
+        }
+        {
+            String ret = AdHocUtil.replaceIdentifierInExpr("a.b.x * a.b.y", "c", false);
+            Assert.assertEquals("c.x * c.y", ret);
+        }
+        {
+            String ret = AdHocUtil.replaceIdentifierInExpr("a.b.x * a.b.y", "c", true);
+            Assert.assertEquals("\"C\".\"X\" * \"C\".\"Y\"", ret);
+        }
+        {
+            String ret = AdHocUtil.replaceIdentifierInExpr("substr(a.b.x,1,3)>a.b.y", "c", true);
+            Assert.assertEquals("substr(\"C\".\"X\",1,3)>\"C\".\"Y\"", ret);
+        }
+        {
+            String ret = AdHocUtil.replaceIdentifierInExpr("strcmp(substr(a.b.x,1,3),a.b.y) > 0", "c", true);
+            Assert.assertEquals("strcmp(substr(\"C\".\"X\",1,3),\"C\".\"Y\") > 0", ret);
+        }
+        {
+            String ret = AdHocUtil.replaceIdentifierInExpr("strcmp(substr(a.b.x,1,3),a.b.y) > 0", null, true);
+            Assert.assertEquals("strcmp(substr(\"B\".\"X\",1,3),\"B\".\"Y\") > 0", ret);
+        }
+        {
+            String ret = AdHocUtil.replaceIdentifierInExpr("strcmp(substr(a.b.x, 1, 3),a.b.y) > 0", null, false);
+            Assert.assertEquals("strcmp(substr(b.x, 1, 3),b.y) > 0", ret);
+        }
+    }
+
+    @Test
+    public void testRestoreComputedColumnToExpr() {
+
+        ComputedColumnDesc computedColumnDesc = mock(ComputedColumnDesc.class);
+        when(computedColumnDesc.getColumnName()).thenReturn("DEAL_AMOUNT");
+        when(computedColumnDesc.getExpression()).thenReturn("DB.TABLE.price * DB.TABLE.number");
+
+        CCInfo ccInfo = mock(CCInfo.class);
+        when(ccInfo.getComputedColumnDesc()).thenReturn(computedColumnDesc);
+
+        {
+            String ret = AdHocUtil.restoreComputedColumnToExpr(
+                    "select DEAL_AMOUNT from DB.TABLE group by date order by DEAL_AMOUNT", ccInfo);
+            Assert.assertEquals(
+                    "select (TABLE.price * TABLE.number) from DB.TABLE group by date order by (TABLE.price * TABLE.number)",
+                    ret);
+        }
+        {
+            String ret = AdHocUtil.restoreComputedColumnToExpr(
+                    "select DEAL_AMOUNT as DEAL_AMOUNT from DB.TABLE group by date order by DEAL_AMOUNT", ccInfo);
+            Assert.assertEquals(
+                    "select (TABLE.price * TABLE.number) as DEAL_AMOUNT from DB.TABLE group by date order by (TABLE.price * TABLE.number)",
+                    ret);
+        }
+        {
+            String ret = AdHocUtil.restoreComputedColumnToExpr(
+                    "select \"DEAL_AMOUNT\" AS deal_amount from DB.TABLE group by date order by DEAL_AMOUNT", ccInfo);
+            Assert.assertEquals(
+                    "select (\"TABLE\".\"PRICE\" * \"TABLE\".\"NUMBER\") AS deal_amount from DB.TABLE group by date order by (TABLE.price * TABLE.number)",
+                    ret);
+        }
+    }
+}