You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@asterixdb.apache.org by dl...@apache.org on 2019/08/07 22:11:44 UTC

[asterixdb] branch master updated: [NO ISSUE][COMP] Improve warnings for SQL++ hints

This is an automated email from the ASF dual-hosted git repository.

dlych pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/asterixdb.git


The following commit(s) were added to refs/heads/master by this push:
     new e77cdac  [NO ISSUE][COMP] Improve warnings for SQL++ hints
e77cdac is described below

commit e77cdacb5939b930db20620012dc91ed6392a1e1
Author: Dmitry Lychagin <dm...@couchbase.com>
AuthorDate: Mon Aug 5 11:37:47 2019 -0700

    [NO ISSUE][COMP] Improve warnings for SQL++ hints
    
    - user model changes: no
    - storage format changes: no
    - interface changes: no
    
    Details:
    - Issue warnings for all hints that were not processed
      by the SQL++ parser. A hint is not processed because
      it is either unknown or unexpected at given location
    
    Change-Id: Iec46335a8db4360e5431b145cd9e98c7075c7fda
    Reviewed-on: https://asterix-gerrit.ics.uci.edu/3517
    Contrib: Jenkins <je...@fulliautomatix.ics.uci.edu>
    Integration-Tests: Jenkins <je...@fulliautomatix.ics.uci.edu>
    Tested-by: Jenkins <je...@fulliautomatix.ics.uci.edu>
    Reviewed-by: Dmitry Lychagin <dm...@couchbase.com>
    Reviewed-by: Ali Alsuliman <al...@gmail.com>
---
 .../inapplicable-hint-warning.1.query.sqlpp        |   6 +-
 ...lpp => inapplicable-hint-warning.2.query.sqlpp} |   9 +-
 ...lpp => inapplicable-hint-warning.3.query.sqlpp} |  17 +-
 ...lpp => inapplicable-hint-warning.4.query.sqlpp} |  17 +-
 ...lpp => inapplicable-hint-warning.5.query.sqlpp} |  17 +-
 ...lpp => inapplicable-hint-warning.6.query.sqlpp} |  17 +-
 .../unknown-hint-warning.5.query.sqlpp}            |  16 +-
 .../inapplicable-hint-warning.2.adm                |   2 +
 .../inapplicable-hint-warning.3.adm                |   1 +
 .../inapplicable-hint-warning.4.adm                |   1 +
 .../inapplicable-hint-warning.5.adm                |   1 +
 .../inapplicable-hint-warning.6.adm                |   1 +
 .../unknown-hint-warning.5.adm                     |   1 +
 .../test/resources/runtimets/testsuite_sqlpp.xml   |  14 +-
 .../asterix/common/exceptions/ErrorCode.java       |   2 +-
 .../src/main/resources/asx_errormsg/en.properties  |   2 +-
 .../asterix/lang/sqlpp/parser/SqlppHint.java       |  93 +++++
 .../asterix/lang/sqlpp/parser/SqlppToken.java      |  34 +-
 .../asterix-lang-sqlpp/src/main/javacc/SQLPP.jj    | 374 +++++++++++----------
 .../java/org/apache/hyracks/util/StringUtil.java   |  14 +
 20 files changed, 369 insertions(+), 270 deletions(-)

diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
index 0c0e667..2b70df1 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
@@ -18,9 +18,9 @@
  */
 
 /*
- * Description  : Warning when a GROUP BY hint is recognized,
- *              : but cannot be applied for given aggregate function
- * Expected     : SUCCESS (with ASX1107 warning)
+ * Description  : Warning when a GROUP BY hint is the expected one,
+ *              : but cannot be applied for a given aggregate function
+ * Expected     : SUCCESS (with HYR10006 warning)
  */
 
 with ds as (
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.2.query.sqlpp
similarity index 82%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.2.query.sqlpp
index 0c0e667..d0d6c1a 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.2.query.sqlpp
@@ -18,8 +18,8 @@
  */
 
 /*
- * Description  : Warning when a GROUP BY hint is recognized,
- *              : but cannot be applied for given aggregate function
+ * Description  : Warning when a hint at GROUP BY is recognized,
+ *              : but not applicable for GROUP BY
  * Expected     : SUCCESS (with ASX1107 warning)
  */
 
@@ -29,7 +29,6 @@ with ds as (
 )
 
 from ds
-where y < 3
-/*+ hash */ group by x
-select x, array_agg(y) as y
+/*+ indexnl */ group by x
+select x, sum(y) as y
 order by x
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.3.query.sqlpp
similarity index 75%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.3.query.sqlpp
index 0c0e667..daab311 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.3.query.sqlpp
@@ -18,18 +18,11 @@
  */
 
 /*
- * Description  : Warning when a GROUP BY hint is recognized,
- *              : but cannot be applied for given aggregate function
+ * Description  : Warning when a hint at relational expression is recognized
+ *              : but not applicable for relational expression
  * Expected     : SUCCESS (with ASX1107 warning)
  */
 
-with ds as (
-  from range(1, 4) r
-  select r % 2 as x, r as y
-)
-
-from ds
-where y < 3
-/*+ hash */ group by x
-select x, array_agg(y) as y
-order by x
\ No newline at end of file
+from range(1, 4) r
+where r /*+ hash */ < 2
+select value r
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.4.query.sqlpp
similarity index 75%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.4.query.sqlpp
index 0c0e667..cc8c8df 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.4.query.sqlpp
@@ -18,18 +18,11 @@
  */
 
 /*
- * Description  : Warning when a GROUP BY hint is recognized,
- *              : but cannot be applied for given aggregate function
+ * Description  : Warning when a hint at BETWEEN is recognized
+ *              : but not applicable for BETWEEN
  * Expected     : SUCCESS (with ASX1107 warning)
  */
 
-with ds as (
-  from range(1, 4) r
-  select r % 2 as x, r as y
-)
-
-from ds
-where y < 3
-/*+ hash */ group by x
-select x, array_agg(y) as y
-order by x
\ No newline at end of file
+from range(1, 4) r
+where r /*+ auto */ between 0 and 1
+select value r
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.5.query.sqlpp
similarity index 75%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.5.query.sqlpp
index 0c0e667..a19aeec 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.5.query.sqlpp
@@ -18,18 +18,11 @@
  */
 
 /*
- * Description  : Warning when a GROUP BY hint is recognized,
- *              : but cannot be applied for given aggregate function
+ * Description  : Warning when a hint at function call is recognized
+ *              : but not applicable for function call
  * Expected     : SUCCESS (with ASX1107 warning)
  */
 
-with ds as (
-  from range(1, 4) r
-  select r % 2 as x, r as y
-)
-
-from ds
-where y < 3
-/*+ hash */ group by x
-select x, array_agg(y) as y
-order by x
\ No newline at end of file
+from range(1, 4) r
+where /*+ hash */ tostring(r) < "2"
+select value r
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.6.query.sqlpp
similarity index 75%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.6.query.sqlpp
index 0c0e667..b669569 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.6.query.sqlpp
@@ -18,18 +18,11 @@
  */
 
 /*
- * Description  : Warning when a GROUP BY hint is recognized,
- *              : but cannot be applied for given aggregate function
+ * Description  : Warning when a hint is recognized
+ *              : but no hints applicable at this location
  * Expected     : SUCCESS (with ASX1107 warning)
  */
 
-with ds as (
-  from range(1, 4) r
-  select r % 2 as x, r as y
-)
-
-from ds
-where y < 3
-/*+ hash */ group by x
-select x, array_agg(y) as y
-order by x
\ No newline at end of file
+from range(1, 4) r
+where r < 2
+select /*+ hash */ value r
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/unknown-hint-warning/unknown-hint-warning.5.query.sqlpp
similarity index 75%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/unknown-hint-warning/unknown-hint-warning.5.query.sqlpp
index 0c0e667..517e39b 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/inapplicable-hint-warning/inapplicable-hint-warning.1.query.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/warnings/unknown-hint-warning/unknown-hint-warning.5.query.sqlpp
@@ -18,18 +18,10 @@
  */
 
 /*
- * Description  : Warning when a GROUP BY hint is recognized,
- *              : but cannot be applied for given aggregate function
+ * Description  : Warning when a hint is not recognized elsewhere
  * Expected     : SUCCESS (with ASX1107 warning)
  */
 
-with ds as (
-  from range(1, 4) r
-  select r % 2 as x, r as y
-)
-
-from ds
-where y < 3
-/*+ hash */ group by x
-select x, array_agg(y) as y
-order by x
\ No newline at end of file
+from range(1, 4) r
+where r < 2
+select /*+ unknown_hint_elsewhere */ value r
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.2.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.2.adm
new file mode 100644
index 0000000..60bff22
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.2.adm
@@ -0,0 +1,2 @@
+{ "x": 0, "y": 6 }
+{ "x": 1, "y": 4 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.3.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.3.adm
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.3.adm
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.4.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.4.adm
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.4.adm
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.5.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.5.adm
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.5.adm
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.6.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.6.adm
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/inapplicable-hint-warning/inapplicable-hint-warning.6.adm
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/unknown-hint-warning/unknown-hint-warning.5.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/unknown-hint-warning/unknown-hint-warning.5.adm
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/warnings/unknown-hint-warning/unknown-hint-warning.5.adm
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
index c2c3f4c..0724f45 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
@@ -12418,6 +12418,11 @@
       <compilation-unit name="inapplicable-hint-warning">
         <output-dir compare="Text">inapplicable-hint-warning</output-dir>
         <expected-warn>HYR10006: Could not apply Group By hint: hash</expected-warn>
+        <expected-warn>ASX1107: Unexpected hint: indexnl. "hash" expected at this location</expected-warn>
+        <expected-warn>ASX1107: Unexpected hint: hash. "indexnl", "skip-index", "bcast" expected at this location</expected-warn>
+        <expected-warn>ASX1107: Unexpected hint: auto. "indexnl", "skip-index" expected at this location</expected-warn>
+        <expected-warn>ASX1107: Unexpected hint: hash. "indexnl", "skip-index" expected at this location</expected-warn>
+        <expected-warn>ASX1107: Unexpected hint: hash. None expected at this location</expected-warn>
       </compilation-unit>
     </test-case>
     <test-case FilePath="warnings" check-warnings="true">
@@ -12436,10 +12441,11 @@
     <test-case FilePath="warnings" check-warnings="true">
       <compilation-unit name="unknown-hint-warning">
         <output-dir compare="Text">unknown-hint-warning</output-dir>
-        <expected-warn>ASX1107: Unknown hint: unknown_hint_groupby. Supported hints are: hash</expected-warn>
-        <expected-warn>ASX1107: Unknown hint: unknown_hint_relexpr. Supported hints are: indexnl, skip-index, bcast</expected-warn>
-        <expected-warn>ASX1107: Unknown hint: unknown_hint_between. Supported hints are: indexnl, skip-index</expected-warn>
-        <expected-warn>ASX1107: Unknown hint: unknown_hint_funcall. Supported hints are: indexnl, skip-index</expected-warn>
+        <expected-warn>ASX1107: Unexpected hint: unknown_hint_groupby. "hash" expected at this location</expected-warn>
+        <expected-warn>ASX1107: Unexpected hint: unknown_hint_relexpr. "indexnl", "skip-index", "bcast" expected at this location</expected-warn>
+        <expected-warn>ASX1107: Unexpected hint: unknown_hint_between. "indexnl", "skip-index" expected at this location</expected-warn>
+        <expected-warn>ASX1107: Unexpected hint: unknown_hint_funcall. "indexnl", "skip-index" expected at this location</expected-warn>
+        <expected-warn>ASX1107: Unexpected hint: unknown_hint_elsewhere. None expected at this location</expected-warn>
       </compilation-unit>
     </test-case>
   </test-group>
diff --git a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
index 188ad94..ca8ee38 100644
--- a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
+++ b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
@@ -188,7 +188,7 @@ public class ErrorCode {
     public static final int INVALID_FUNCTION_MODIFIER = 1104;
     public static final int OPERATION_NOT_SUPPORTED_ON_PRIMARY_INDEX = 1105;
     public static final int EXPECTED_CONSTANT_VALUE = 1106;
-    public static final int UNKNOWN_HINT = 1107;
+    public static final int UNEXPECTED_HINT = 1107;
 
     // Feed errors
     public static final int DATAFLOW_ILLEGAL_STATE = 3001;
diff --git a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
index 0848f66..362e506 100644
--- a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
+++ b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
@@ -183,7 +183,7 @@
 1104 = Invalid modifier %1$s for function %2$s
 1105 = Operation not supported on primary index %1$s
 1106 = Expected constant value
-1107 = Unknown hint: %1$s. Supported hints are: %2$s
+1107 = Unexpected hint: %1$s. %2$s expected at this location
 
 # Feed Errors
 3001 = Illegal state.
diff --git a/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/parser/SqlppHint.java b/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/parser/SqlppHint.java
new file mode 100644
index 0000000..4770581
--- /dev/null
+++ b/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/parser/SqlppHint.java
@@ -0,0 +1,93 @@
+/*
+ * 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.asterix.lang.sqlpp.parser;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+public enum SqlppHint {
+
+    // optimizer hints
+    AUTO_HINT("auto"),
+    BROADCAST_JOIN_HINT("bcast"),
+    COMPOSE_VAL_FILES_HINT("compose-val-files"),
+    DATE_BETWEEN_YEARS_HINT("date-between-years"),
+    DATETIME_ADD_RAND_HOURS_HINT("datetime-add-rand-hours"),
+    DATETIME_BETWEEN_YEARS_HINT("datetime-between-years"),
+    HASH_GROUP_BY_HINT("hash"),
+    INDEXED_NESTED_LOOP_JOIN_HINT("indexnl"),
+    INMEMORY_HINT("inmem"),
+    INSERT_RAND_INT_HINT("insert-rand-int"),
+    INTERVAL_HINT("interval"),
+    LIST_HINT("list"),
+    LIST_VAL_FILE_HINT("list-val-file"),
+    RANGE_HINT("range"),
+    SKIP_SECONDARY_INDEX_SEARCH_HINT("skip-index"),
+    VAL_FILE_HINT("val-files"),
+    VAL_FILE_SAME_INDEX_HINT("val-file-same-idx"),
+    GEN_FIELDS_HINT("gen-fields"),
+
+    // data generator hints
+    DGEN_HINT("dgen");
+
+    private static final Map<String, SqlppHint> ID_MAP = createIdentifierMap(values());
+
+    private final String id;
+
+    SqlppHint(String id) {
+        Objects.requireNonNull(id);
+        this.id = id;
+    }
+
+    public String getIdentifier() {
+        return id;
+    }
+
+    @Override
+    public String toString() {
+        return getIdentifier();
+    }
+
+    public static SqlppHint findByIdentifier(String id) {
+        return ID_MAP.get(id);
+    }
+
+    private static Map<String, SqlppHint> createIdentifierMap(SqlppHint[] values) {
+        Map<String, SqlppHint> map = new HashMap<>();
+        for (SqlppHint hint : values) {
+            map.put(hint.getIdentifier(), hint);
+        }
+        return map;
+    }
+
+    public static int findParamStart(String str) {
+        for (int i = 0, ln = str.length(); i < ln; i++) {
+            if (!isIdentifierChar(str.charAt(i))) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    public static boolean isIdentifierChar(char c) {
+        return Character.isJavaIdentifierStart(c) || c == '-';
+    }
+}
diff --git a/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/StringUtil.java b/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/parser/SqlppToken.java
similarity index 52%
copy from hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/StringUtil.java
copy to asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/parser/SqlppToken.java
index 11be0ba..13dca02 100644
--- a/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/StringUtil.java
+++ b/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/parser/SqlppToken.java
@@ -16,24 +16,30 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.hyracks.util;
 
-import java.util.Collections;
-import java.util.Map;
-import java.util.regex.Pattern;
+package org.apache.asterix.lang.sqlpp.parser;
 
-import org.apache.commons.collections4.map.LRUMap;
-import org.apache.commons.text.WordUtils;
+import java.io.Serializable;
 
-public class StringUtil {
-    private static final Map<String, String> CAMEL_CACHE = Collections.synchronizedMap(new LRUMap<>(1024));
-    private static final Pattern SEPARATORS_PATTERN = Pattern.compile("[_\\-\\s]");
+import org.apache.hyracks.api.exceptions.SourceLocation;
 
-    private StringUtil() {
-    }
+public abstract class SqlppToken implements Serializable {
+
+    public SourceLocation sourceLocation;
+
+    public SqlppHint hint;
+    public String hintParams;
 
-    public static String toCamelCase(String input) {
-        return CAMEL_CACHE.computeIfAbsent(input, s -> SEPARATORS_PATTERN
-                .matcher(WordUtils.capitalize("z" + s.toLowerCase(), '_', '-', ' ').substring(1)).replaceAll(""));
+    public boolean parseHint(String text) {
+        int paramStart = SqlppHint.findParamStart(text);
+        String id = paramStart >= 0 ? text.substring(0, paramStart) : text;
+        hint = SqlppHint.findByIdentifier(id);
+        if (hint != null) {
+            hintParams = paramStart >= 0 ? text.substring(paramStart).trim() : null;
+            return true;
+        } else {
+            hintParams = text;
+            return false;
+        }
     }
 }
diff --git a/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj b/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
index ead92cf..0909fe0 100644
--- a/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
+++ b/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
@@ -17,13 +17,11 @@
 // under the License.
 //
 options {
-
-
-       STATIC = false;
-
+  COMMON_TOKEN_ACTION = true;
+  STATIC = false;
+  TOKEN_EXTENDS = "org.apache.asterix.lang.sqlpp.parser.SqlppToken";
 }
 
-
 PARSER_BEGIN(SQLPPParser)
 
 package org.apache.asterix.lang.sqlpp.parser;
@@ -176,12 +174,14 @@ import org.apache.asterix.lang.sqlpp.expression.SelectExpression;
 import org.apache.asterix.lang.sqlpp.expression.WindowExpression;
 import org.apache.asterix.lang.sqlpp.optype.JoinType;
 import org.apache.asterix.lang.sqlpp.optype.SetOpType;
+import org.apache.asterix.lang.sqlpp.parser.SqlppHint;
 import org.apache.asterix.lang.sqlpp.struct.SetOperationInput;
 import org.apache.asterix.lang.sqlpp.struct.SetOperationRight;
 import org.apache.asterix.lang.sqlpp.util.ExpressionToVariableUtil;
 import org.apache.asterix.lang.sqlpp.util.FunctionMapUtil;
 import org.apache.asterix.lang.sqlpp.util.SqlppVariableUtil;
 import org.apache.asterix.om.functions.BuiltinFunctions;
+import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hyracks.algebricks.common.utils.Pair;
 import org.apache.hyracks.algebricks.common.utils.Triple;
@@ -192,6 +192,7 @@ import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
 import org.apache.hyracks.api.exceptions.SourceLocation;
 import org.apache.hyracks.api.exceptions.Warning;
 import org.apache.hyracks.util.LogRedactionUtil;
+import org.apache.hyracks.util.StringUtil;
 
 class SQLPPParser extends ScopeChecker implements IParser {
 
@@ -215,29 +216,6 @@ class SQLPPParser extends ScopeChecker implements IParser {
     private static final String TIES = "TIES";
     private static final String UNBOUNDED = "UNBOUNDED";
 
-    // optimizer hints
-    private static final String AUTO_HINT = "auto";
-    private static final String BROADCAST_JOIN_HINT = "bcast";
-    private static final String COMPOSE_VAL_FILES_HINT = "compose-val-files";
-    private static final String DATE_BETWEEN_YEARS_HINT = "date-between-years";
-    private static final String DATETIME_ADD_RAND_HOURS_HINT = "datetime-add-rand-hours";
-    private static final String DATETIME_BETWEEN_YEARS_HINT = "datetime-between-years";
-    private static final String HASH_GROUP_BY_HINT = "hash";
-    private static final String INDEXED_NESTED_LOOP_JOIN_HINT = "indexnl";
-    private static final String INMEMORY_HINT = "inmem";
-    private static final String INSERT_RAND_INT_HINT = "insert-rand-int";
-    private static final String INTERVAL_HINT = "interval";
-    private static final String LIST_HINT = "list";
-    private static final String LIST_VAL_FILE_HINT = "list-val-file";
-    private static final String RANGE_HINT = "range";
-    private static final String SKIP_SECONDARY_INDEX_SEARCH_HINT = "skip-index";
-    private static final String VAL_FILE_HINT = "val-files";
-    private static final String VAL_FILE_SAME_INDEX_HINT = "val-file-same-idx";
-    private static final String GEN_FIELDS_HINT = "gen-fields";
-
-    // data generator hints
-    private static final String DGEN_HINT = "dgen";
-
     // error configuration
     protected static final boolean REPORT_EXPECTED_TOKENS = false;
 
@@ -245,6 +223,8 @@ class SQLPPParser extends ScopeChecker implements IParser {
 
     private final WarningCollector warningCollector = new WarningCollector();
 
+    private final Map<SourceLocation, String> hintCollector = new HashMap<SourceLocation, String>();
+
     private static class IndexParams {
       public IndexType type;
       public int gramLength;
@@ -259,73 +239,64 @@ class SQLPPParser extends ScopeChecker implements IParser {
        public String dataverse;
        public String library;
        public String function;
-       public String hint;
+       public SqlppHint hint;
        public SourceLocation sourceLoc;
-       public SourceLocation hintSourceLoc;
     }
 
-    private String getHint(Token t) {
-        if (t.specialToken == null) {
-            return null;
-        }
-        String s = t.specialToken.image;
-        int n = s.length();
-        if (n < 2) {
-            return null;
-        }
-        return s.substring(1).trim();
-    }
-
-    private Token getHintToken(Token t) {
-        return t.specialToken;
-    }
-
-    private IRecordFieldDataGen parseFieldDataGen(String hint, Token hintToken) throws ParseException {
-      IRecordFieldDataGen rfdg = null;
-      String splits[] = hint.split(" +");
-      if (splits[0].equals(VAL_FILE_HINT)) {
-        File[] valFiles = new File[splits.length - 1];
-        for (int k=1; k<splits.length; k++) {
-          valFiles[k-1] = new File(splits[k]);
-        }
-        rfdg = new FieldValFileDataGen(valFiles);
-      } else if (splits[0].equals(VAL_FILE_SAME_INDEX_HINT)) {
-        rfdg = new FieldValFileSameIndexDataGen(new File(splits[1]), splits[2]);
-      } else if (splits[0].equals(LIST_VAL_FILE_HINT)) {
-        rfdg = new ListValFileDataGen(new File(splits[1]), Integer.parseInt(splits[2]), Integer.parseInt(splits[3]));
-      } else if (splits[0].equals(LIST_HINT)) {
-        rfdg = new ListDataGen(Integer.parseInt(splits[1]), Integer.parseInt(splits[2]));
-      } else if (splits[0].equals(INTERVAL_HINT)) {
-        FieldIntervalDataGen.ValueType vt;
-        if (splits[1].equals("int")) {
-          vt = FieldIntervalDataGen.ValueType.INT;
-        } else if (splits[1].equals("long")) {
-          vt = FieldIntervalDataGen.ValueType.LONG;
-        } else if (splits[1].equals("float")) {
-          vt = FieldIntervalDataGen.ValueType.FLOAT;
-        } else if (splits[1].equals("double")) {
-          vt = FieldIntervalDataGen.ValueType.DOUBLE;
-        } else {
-          throw new SqlppParseException(getSourceLocation(hintToken), "Unknown type for interval data gen: " + splits[1]);
-        }
-        rfdg = new FieldIntervalDataGen(vt, splits[2], splits[3]);
-      } else if (splits[0].equals(INSERT_RAND_INT_HINT)) {
-        rfdg = new InsertRandIntDataGen(splits[1], splits[2]);
-      } else if (splits[0].equals(DATE_BETWEEN_YEARS_HINT)) {
-        rfdg = new DateBetweenYearsDataGen(Integer.parseInt(splits[1]), Integer.parseInt(splits[2]));
-      } else if (splits[0].equals(DATETIME_BETWEEN_YEARS_HINT)) {
-        rfdg = new DatetimeBetweenYearsDataGen(Integer.parseInt(splits[1]), Integer.parseInt(splits[2]));
-      } else if (splits[0].equals(DATETIME_ADD_RAND_HOURS_HINT)) {
-        rfdg = new DatetimeAddRandHoursDataGen(Integer.parseInt(splits[1]), Integer.parseInt(splits[2]), splits[3]);
-      } else if (splits[0].equals(AUTO_HINT)) {
-        rfdg = new AutoDataGen(splits[1]);
+    private IRecordFieldDataGen parseFieldDataGen(Token hintToken) throws ParseException {
+      String[] splits = hintToken.hintParams != null ? hintToken.hintParams.split("\\s+") : null;
+      switch (hintToken.hint) {
+        case VAL_FILE_HINT:
+          File[] valFiles = new File[splits.length];
+          for (int k=0; k<splits.length; k++) {
+            valFiles[k] = new File(splits[k]);
+          }
+          return new FieldValFileDataGen(valFiles);
+        case VAL_FILE_SAME_INDEX_HINT:
+          return new FieldValFileSameIndexDataGen(new File(splits[0]), splits[1]);
+        case LIST_VAL_FILE_HINT:
+          return new ListValFileDataGen(new File(splits[0]), Integer.parseInt(splits[1]), Integer.parseInt(splits[2]));
+        case LIST_HINT:
+          return new ListDataGen(Integer.parseInt(splits[0]), Integer.parseInt(splits[1]));
+        case INTERVAL_HINT:
+          FieldIntervalDataGen.ValueType vt;
+          switch (splits[0]) {
+            case "int":
+              vt = FieldIntervalDataGen.ValueType.INT;
+              break;
+            case "long":
+              vt = FieldIntervalDataGen.ValueType.LONG;
+              break;
+            case "float":
+              vt = FieldIntervalDataGen.ValueType.FLOAT;
+              break;
+            case "double":
+              vt = FieldIntervalDataGen.ValueType.DOUBLE;
+              break;
+            default:
+              throw new SqlppParseException(getSourceLocation(hintToken),
+                "Unknown type for interval data gen: " + splits[0]);
+          }
+          return new FieldIntervalDataGen(vt, splits[1], splits[2]);
+        case INSERT_RAND_INT_HINT:
+          return new InsertRandIntDataGen(splits[0], splits[1]);
+        case DATE_BETWEEN_YEARS_HINT:
+          return new DateBetweenYearsDataGen(Integer.parseInt(splits[0]), Integer.parseInt(splits[1]));
+        case DATETIME_BETWEEN_YEARS_HINT:
+          return new DatetimeBetweenYearsDataGen(Integer.parseInt(splits[0]), Integer.parseInt(splits[1]));
+        case DATETIME_ADD_RAND_HOURS_HINT:
+          return new DatetimeAddRandHoursDataGen(Integer.parseInt(splits[0]), Integer.parseInt(splits[1]), splits[2]);
+        case AUTO_HINT:
+          return new AutoDataGen(splits[0]);
+        default:
+          return null;
       }
-      return rfdg;
     }
 
     public SQLPPParser(String s) {
         this(new StringReader(s));
         super.setInput(s);
+        token_source.hintCollector = hintCollector;
     }
 
     public static void main(String args[]) throws ParseException, TokenMgrError, IOException, FileNotFoundException, CompilationException {
@@ -360,6 +331,7 @@ class SQLPPParser extends ScopeChecker implements IParser {
 
     private <T> T parseImpl(ParseFunction<T> parseFunction) throws CompilationException {
         warningCollector.clear();
+        hintCollector.clear();
         try {
             return parseFunction.parse();
         } catch (Error e) {
@@ -371,6 +343,8 @@ class SQLPPParser extends ScopeChecker implements IParser {
             throw new CompilationException(ErrorCode.PARSE_ERROR, e.getSourceLocation(), LogRedactionUtil.userData(getMessage(e)));
         } catch (ParseException e) {
             throw new CompilationException(ErrorCode.PARSE_ERROR, LogRedactionUtil.userData(getMessage(e)));
+        } finally {
+            reportUnclaimedHints();
         }
     }
 
@@ -426,7 +400,10 @@ class SQLPPParser extends ScopeChecker implements IParser {
     }
 
     protected static SourceLocation getSourceLocation(Token token) {
-        return token != null ? new SourceLocation(token.beginLine, token.beginColumn) : null;
+        return
+          token == null ? null :
+          token.sourceLocation != null ? token.sourceLocation :
+          new SourceLocation(token.beginLine, token.beginColumn);
     }
 
     protected static <T extends AbstractLangExpression> T addSourceLocation(T expr, Token token) {
@@ -461,15 +438,41 @@ class SQLPPParser extends ScopeChecker implements IParser {
       return laIdentifier(1, image);
     }
 
-    private void warnUnknownHint(String actualHint, SourceLocation sourceLoc, String... expectedHints) {
-        warningCollector.warn(WarningUtil.forAsterix(sourceLoc, ErrorCode.UNKNOWN_HINT, actualHint,
-          StringUtils.join(expectedHints, ", ")));
+    private Token fetchHint(Token token, SqlppHint... expectedHints) {
+      Token hintToken = token.specialToken;
+      if (hintToken == null) {
+        return null;
+      }
+      SourceLocation sourceLoc = getSourceLocation(hintToken);
+      hintCollector.remove(sourceLoc);
+      if (hintToken.hint == null) {
+        warnUnexpectedHint(hintToken.hintParams, sourceLoc, expectedHints);
+        return null;
+      } else if (!ArrayUtils.contains(expectedHints, hintToken.hint)) {
+        warnUnexpectedHint(hintToken.hint.getIdentifier(), sourceLoc, expectedHints);
+        return null;
+      } else {
+        return hintToken;
+      }
+    }
+
+    private void reportUnclaimedHints() {
+        for (Map.Entry<SourceLocation, String> me : hintCollector.entrySet()) {
+            warnUnexpectedHint(me.getValue(), me.getKey(), "None");
+        }
+    }
+
+    private void warnUnexpectedHint(String actualHint, SourceLocation sourceLoc, SqlppHint... expectedHints) {
+        warnUnexpectedHint(actualHint, sourceLoc, StringUtil.join(expectedHints, ", ", "\""));
+    }
+
+    private void warnUnexpectedHint(String actualHint, SourceLocation sourceLoc, String expectedHints) {
+        warningCollector.warn(WarningUtil.forAsterix(sourceLoc, ErrorCode.UNEXPECTED_HINT, actualHint, expectedHints));
     }
 }
 
 PARSER_END(SQLPPParser)
 
-
 List<Statement> Statement() throws ParseException:
 {
   scopeStack.push(RootScopeFactory.createRootScope(this));
@@ -556,22 +559,12 @@ DataverseDecl DataverseDeclaration() throws ParseException:
 Statement CreateStatement() throws ParseException:
 {
   Token startToken = null;
-  String hint = null;
-  Token hintToken = null;
-  boolean hintDGen = false;
   Statement stmt = null;
 }
 {
   <CREATE> { startToken = token; }
   (
-    {
-      hint = getHint(token);
-      if (hint != null) {
-        hintToken = getHintToken(token);
-        hintDGen = hint.startsWith(DGEN_HINT);
-      }
-    }
-    stmt = TypeSpecification(startToken, hint, hintDGen, hintToken)
+    stmt = TypeSpecification(startToken)
     | stmt = NodegroupSpecification(startToken)
     | stmt = DatasetSpecification(startToken)
     | stmt = IndexSpecification(startToken)
@@ -585,7 +578,7 @@ Statement CreateStatement() throws ParseException:
   }
 }
 
-TypeDecl TypeSpecification(Token startStmtToken, String hint, boolean dgen, Token hintToken) throws ParseException:
+TypeDecl TypeSpecification(Token startStmtToken) throws ParseException:
 {
   Pair<Identifier,Identifier> nameComponents = null;
   boolean ifNotExists = false;
@@ -595,15 +588,20 @@ TypeDecl TypeSpecification(Token startStmtToken, String hint, boolean dgen, Toke
   <TYPE> nameComponents = TypeName() ifNotExists = IfNotExists()
   <AS> typeExpr = RecordTypeDef()
     {
+      boolean dgen = false;
       long numValues = -1;
       String filename = null;
-      if (dgen) {
-        String splits[] = hint.split(" +");
-        if (splits.length != 3) {
-          throw new SqlppParseException(getSourceLocation(hintToken), "Expecting /*+ dgen <filename> <numberOfItems> */");
-        }
-        filename = splits[1];
-        numValues = Long.parseLong(splits[2]);
+      Token hintToken = fetchHint(startStmtToken, SqlppHint.DGEN_HINT);
+      if (hintToken != null) {
+          String hintParams = hintToken.hintParams;
+          String[] splits = hintParams != null ? hintParams.split("\\s+") : null;
+          if (splits == null || splits.length != 2) {
+            throw new SqlppParseException(getSourceLocation(hintToken),
+              "Expecting /*+ dgen <filename> <numberOfItems> */");
+          }
+          dgen = true;
+          filename = splits[0];
+          numValues = Long.parseLong(splits[1]);
       }
       TypeDataGen tddg = new TypeDataGen(dgen, filename, numValues);
       TypeDecl stmt = new TypeDecl(nameComponents.first, nameComponents.second, typeExpr, tddg, ifNotExists);
@@ -1581,29 +1579,27 @@ RecordTypeDefinition RecordTypeDef() throws ParseException:
    <LEFTBRACE>
     {
       startToken = token;
-      String hint = getHint(token);
-      if (hint != null) {
-        String splits[] = hint.split(" +");
-        if (splits[0].equals(GEN_FIELDS_HINT)) {
-          if (splits.length != 5) {
-            throw new SqlppParseException(getSourceLocation(getHintToken(token)),
-                "Expecting: /*+ gen-fields <type> <min> <max> <prefix>*/");
-          }
-          if (!splits[1].equals("int")) {
-            throw new SqlppParseException(getSourceLocation(getHintToken(token)),
-                "The only supported type for gen-fields is int.");
-          }
-          UndeclaredFieldsDataGen ufdg = new UndeclaredFieldsDataGen(UndeclaredFieldsDataGen.Type.INT,
-             Integer.parseInt(splits[2]), Integer.parseInt(splits[3]), splits[4]);
-          recType.setUndeclaredFieldsDataGen(ufdg);
+      Token hintToken = fetchHint(token, SqlppHint.GEN_FIELDS_HINT);
+      if (hintToken != null) {
+        String hintParams = hintToken.hintParams;
+        String[] splits = hintParams != null ? hintParams.split("\\s+") : null;
+        if (splits == null || splits.length != 4) {
+          throw new SqlppParseException(getSourceLocation(hintToken),
+            "Expecting: /*+ gen-fields <type> <min> <max> <prefix>*/");
+        }
+        if (!splits[0].equals("int")) {
+          throw new SqlppParseException(getSourceLocation(hintToken),
+            "The only supported type for gen-fields is int.");
         }
+        UndeclaredFieldsDataGen ufdg = new UndeclaredFieldsDataGen(UndeclaredFieldsDataGen.Type.INT,
+          Integer.parseInt(splits[1]), Integer.parseInt(splits[2]), splits[3]);
+          recType.setUndeclaredFieldsDataGen(ufdg);
       }
-
     }
-        (
-          RecordField(recType)
-          ( <COMMA>  RecordField(recType) )*
-        )?
+    (
+      RecordField(recType)
+      ( <COMMA>  RecordField(recType) )*
+    )?
    <RIGHTBRACE>
    {
       if (recordKind == null) {
@@ -1623,8 +1619,10 @@ void RecordField(RecordTypeDefinition recType) throws ParseException:
 {
   fieldName = Identifier()
     {
-      String hint = getHint(token);
-      IRecordFieldDataGen rfdg = hint != null ? parseFieldDataGen(hint, token.specialToken) : null;
+      Token hintToken = fetchHint(token, SqlppHint.VAL_FILE_HINT, SqlppHint.VAL_FILE_SAME_INDEX_HINT,
+        SqlppHint.LIST_VAL_FILE_HINT, SqlppHint.LIST_HINT, SqlppHint.INTERVAL_HINT, SqlppHint.INSERT_RAND_INT_HINT,
+        SqlppHint.DATE_BETWEEN_YEARS_HINT, SqlppHint.DATETIME_ADD_RAND_HOURS_HINT, SqlppHint.AUTO_HINT);
+      IRecordFieldDataGen rfdg = hintToken != null ? parseFieldDataGen(hintToken) : null;
     }
   <COLON> type =  TypeExpr() (<QUES> { nullable = true; } )?
     {
@@ -1689,9 +1687,12 @@ FunctionName FunctionName() throws ParseException:
   first = Identifier()
   {
     FunctionName result = new FunctionName();
-    result.hint = getHint(token);
     result.sourceLoc = getSourceLocation(token);
-    result.hintSourceLoc = getSourceLocation(getHintToken(token));
+    Token hintToken = fetchHint(token, SqlppHint.INDEXED_NESTED_LOOP_JOIN_HINT,
+      SqlppHint.SKIP_SECONDARY_INDEX_SEARCH_HINT);
+    if (hintToken != null) {
+      result.hint = hintToken.hint;
+    }
   }
   ( <DOT> second = Identifier()
     {
@@ -2055,18 +2056,19 @@ Expression RelExpr() throws ParseException:
     (
       LOOKAHEAD(2)( <LT> | <GT> | <LE> | <GE> | <EQ> | <NE> | <LG> |<SIMILAR> | (<NOT> { not = true; })? <IN>)
         {
-          String mhint = getHint(token);
-          if (mhint != null) {
-            if (mhint.equals(INDEXED_NESTED_LOOP_JOIN_HINT)) {
+          Token hintToken = fetchHint(token, SqlppHint.INDEXED_NESTED_LOOP_JOIN_HINT,
+            SqlppHint.SKIP_SECONDARY_INDEX_SEARCH_HINT, SqlppHint.BROADCAST_JOIN_HINT);
+          if (hintToken != null) {
+            switch (hintToken.hint) {
+              case INDEXED_NESTED_LOOP_JOIN_HINT:
                 annotation = IndexedNLJoinExpressionAnnotation.INSTANCE;
-            } else if (mhint.equals(SKIP_SECONDARY_INDEX_SEARCH_HINT)) {
+                break;
+              case SKIP_SECONDARY_INDEX_SEARCH_HINT:
                 annotation = SkipSecondaryIndexSearchExpressionAnnotation.INSTANCE;
-            } else if (mhint.equals(BROADCAST_JOIN_HINT)) {
+                break;
+              case BROADCAST_JOIN_HINT:
                 broadcast = true;
-            } else {
-                warnUnknownHint(mhint, getSourceLocation(getHintToken(token)),
-                    INDEXED_NESTED_LOOP_JOIN_HINT, SKIP_SECONDARY_INDEX_SEARCH_HINT, BROADCAST_JOIN_HINT
-                );
+                break;
             }
           }
 
@@ -2117,16 +2119,16 @@ Expression BetweenExpr() throws ParseException:
       LOOKAHEAD(2)
       (<NOT> { not = true; })? <BETWEEN>
         {
-          String mhint = getHint(token);
-          if (mhint != null) {
-            if (mhint.equals(INDEXED_NESTED_LOOP_JOIN_HINT)) {
+          Token hintToken = fetchHint(token, SqlppHint.INDEXED_NESTED_LOOP_JOIN_HINT,
+            SqlppHint.SKIP_SECONDARY_INDEX_SEARCH_HINT);
+          if (hintToken != null) {
+            switch (hintToken.hint) {
+              case INDEXED_NESTED_LOOP_JOIN_HINT:
                 annotation = IndexedNLJoinExpressionAnnotation.INSTANCE;
-            } else if (mhint.equals(SKIP_SECONDARY_INDEX_SEARCH_HINT)) {
+                break;
+              case SKIP_SECONDARY_INDEX_SEARCH_HINT:
                 annotation = SkipSecondaryIndexSearchExpressionAnnotation.INSTANCE;
-            } else {
-                warnUnknownHint(mhint, getSourceLocation(getHintToken(token)),
-                    INDEXED_NESTED_LOOP_JOIN_HINT, SKIP_SECONDARY_INDEX_SEARCH_HINT
-                );
+                break;
             }
           }
           String operator = token.image.toLowerCase();
@@ -2807,16 +2809,14 @@ Expression FunctionCallExpr() throws ParseException:
         signature = new FunctionSignature(funcName.dataverse, fqFunctionName, arity);
       }
       callExpr = FunctionMapUtil.normalizedListInputFunctions(new CallExpr(signature,argList));
-      String hint = funcName.hint;
-      if (hint != null) {
-        if (hint.startsWith(INDEXED_NESTED_LOOP_JOIN_HINT)) {
-          callExpr.addHint(IndexedNLJoinExpressionAnnotation.INSTANCE);
-        } else if (hint.startsWith(SKIP_SECONDARY_INDEX_SEARCH_HINT)) {
-          callExpr.addHint(SkipSecondaryIndexSearchExpressionAnnotation.INSTANCE);
-        } else {
-          warnUnknownHint(hint, funcName.hintSourceLoc,
-            INDEXED_NESTED_LOOP_JOIN_HINT, SKIP_SECONDARY_INDEX_SEARCH_HINT
-          );
+      if (funcName.hint != null) {
+        switch (funcName.hint) {
+          case INDEXED_NESTED_LOOP_JOIN_HINT:
+            callExpr.addHint(IndexedNLJoinExpressionAnnotation.INSTANCE);
+            break;
+          case SKIP_SECONDARY_INDEX_SEARCH_HINT:
+            callExpr.addHint(SkipSecondaryIndexSearchExpressionAnnotation.INSTANCE);
+            break;
         }
       }
       callExpr.setSourceLocation(funcName.sourceLoc);
@@ -3475,20 +3475,23 @@ OrderbyClause OrderbyClause() throws ParseException :
     <ORDER>
       {
         startToken = token;
-        String hint = getHint(token);
-        if (hint != null) {
-          if (hint.startsWith(INMEMORY_HINT)) {
-            String splits[] = hint.split(" +");
-            int numFrames = Integer.parseInt(splits[1]);
-            int numTuples = Integer.parseInt(splits[2]);
-            oc.setNumFrames(numFrames);
-            oc.setNumTuples(numTuples);
-          } else if (hint.startsWith(RANGE_HINT)) {
-            try {
-              oc.setRangeMap(RangeMapBuilder.parseHint(parseExpression(hint.substring(RANGE_HINT.length()))));
-            } catch (CompilationException e) {
-              throw new SqlppParseException(getSourceLocation(getHintToken(token)), e.getMessage());
-            }
+        Token hintToken = fetchHint(token, SqlppHint.INMEMORY_HINT, SqlppHint.RANGE_HINT);
+        if (hintToken != null) {
+          switch (hintToken.hint) {
+            case INMEMORY_HINT:
+              String[] splits = hintToken.hintParams.split("\\s+");
+              int numFrames = Integer.parseInt(splits[0]);
+              int numTuples = Integer.parseInt(splits[1]);
+              oc.setNumFrames(numFrames);
+              oc.setNumTuples(numTuples);
+              break;
+            case RANGE_HINT:
+              try {
+                oc.setRangeMap(RangeMapBuilder.parseHint(parseExpression(hintToken.hintParams)));
+              } catch (CompilationException e) {
+                throw new SqlppParseException(getSourceLocation(hintToken), e.getMessage());
+              }
+              break;
           }
         }
       }
@@ -3543,13 +3546,9 @@ GroupbyClause GroupbyClause()throws ParseException :
     <GROUP>
       {
          startToken = token;
-         String hint = getHint(token);
-         if (hint != null) {
-            if (hint.equals(HASH_GROUP_BY_HINT)) {
-              gbc.setHashGroupByHint(true);
-            } else {
-              warnUnknownHint(hint, getSourceLocation(getHintToken(token)), HASH_GROUP_BY_HINT);
-            }
+         Token hintToken = fetchHint(token, SqlppHint.HASH_GROUP_BY_HINT);
+         if (hintToken != null) {
+            gbc.setHashGroupByHint(true);
          }
       }
     <BY> (
@@ -3694,6 +3693,7 @@ TOKEN_MGR_DECLS:
 {
     public int commentDepth = 0;
     public ArrayDeque<Integer> lexerStateStack = new ArrayDeque<Integer>();
+    public Map<SourceLocation, String> hintCollector;
 
     public void pushState() {
       lexerStateStack.push( curLexState );
@@ -3710,6 +3710,16 @@ TOKEN_MGR_DECLS:
          throw new TokenMgrError(msg, -1);
       }
     }
+
+    void CommonTokenAction(Token token) {
+      Token hintToken = token.specialToken;
+      if (hintToken != null) {
+        hintToken.sourceLocation = new SourceLocation(hintToken.beginLine, hintToken.beginColumn);
+        String text = hintToken.image.substring(1).trim();
+        boolean hintFound = hintToken.parseHint(text);
+        hintCollector.put(hintToken.sourceLocation, hintFound ? hintToken.hint.getIdentifier() : hintToken.hintParams);
+      }
+    }
 }
 
 <DEFAULT,IN_DBL_BRACE>
diff --git a/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/StringUtil.java b/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/StringUtil.java
index 11be0ba..78155c9 100644
--- a/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/StringUtil.java
+++ b/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/StringUtil.java
@@ -36,4 +36,18 @@ public class StringUtil {
         return CAMEL_CACHE.computeIfAbsent(input, s -> SEPARATORS_PATTERN
                 .matcher(WordUtils.capitalize("z" + s.toLowerCase(), '_', '-', ' ').substring(1)).replaceAll(""));
     }
+
+    public static String join(Object[] objects, String separator, String quote) {
+        if (objects == null || objects.length == 0) {
+            return "";
+        }
+        int length = objects.length;
+        String str0 = String.valueOf(objects[0]);
+        StringBuilder sb = new StringBuilder((str0.length() + 3) * length);
+        sb.append(quote).append(str0).append(quote);
+        for (int i = 1; i < length; i++) {
+            sb.append(separator).append(quote).append(objects[i]).append(quote);
+        }
+        return sb.toString();
+    }
 }