diff --git a/ql/src/java/org/apache/hadoop/hive/ql/parse/BaseSemanticAnalyzer.java b/ql/src/java/org/apache/hadoop/hive/ql/parse/BaseSemanticAnalyzer.java index 4f1e23d7a6..02dc9a283c 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/parse/BaseSemanticAnalyzer.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/BaseSemanticAnalyzer.java @@ -80,6 +80,7 @@ import org.apache.hadoop.hive.ql.metadata.Table; import org.apache.hadoop.hive.ql.metadata.VirtualColumn; import org.apache.hadoop.hive.ql.optimizer.listbucketingpruner.ListBucketingPrunerUtils; +import org.apache.hadoop.hive.ql.parse.ObjectIdentifierParser.ObjectIdentifier; import org.apache.hadoop.hive.ql.parse.type.ExprNodeTypeCheck; import org.apache.hadoop.hive.ql.parse.type.TypeCheckCtx; import org.apache.hadoop.hive.ql.plan.ExprNodeConstantDesc; @@ -370,27 +371,10 @@ public static String getUnescapedName(ASTNode tableOrColumnNode) throws Semantic return getUnescapedName(tableOrColumnNode, null); } + @Deprecated public static Map.Entry getDbTableNamePair(ASTNode tableNameNode) throws SemanticException { - - if (tableNameNode.getType() != HiveParser.TOK_TABNAME || - (tableNameNode.getChildCount() != 1 && tableNameNode.getChildCount() != 2)) { - throw new SemanticException(ASTErrorUtils.getMsg(ErrorMsg.INVALID_TABLE_NAME.getMsg(), tableNameNode)); - } - - if (tableNameNode.getChildCount() == 2) { - String dbName = unescapeIdentifier(tableNameNode.getChild(0).getText()); - String tableName = unescapeIdentifier(tableNameNode.getChild(1).getText()); - if (dbName.contains(".") || tableName.contains(".")) { - throw new SemanticException(ASTErrorUtils.getMsg(ErrorMsg.OBJECTNAME_CONTAINS_DOT.getMsg(), tableNameNode)); - } - return Pair.of(dbName, tableName); - } else { - String tableName = unescapeIdentifier(tableNameNode.getChild(0).getText()); - if (tableName.contains(".")) { - throw new SemanticException(ASTErrorUtils.getMsg(ErrorMsg.OBJECTNAME_CONTAINS_DOT.getMsg(), tableNameNode)); - } - return Pair.of(null,tableName); - } + ObjectIdentifier tableId = ObjectIdentifierParser.parseTableIdentifer(tableNameNode); + return Pair.of(tableId.getParentIdentifier().orElse(null), tableId.getIdentifier()); } public static String getUnescapedName(ASTNode tableOrColumnNode, String currentDatabase) throws SemanticException { diff --git a/ql/src/java/org/apache/hadoop/hive/ql/parse/InvalidBacktickException.java b/ql/src/java/org/apache/hadoop/hive/ql/parse/InvalidBacktickException.java new file mode 100644 index 0000000000..fdbca07205 --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/InvalidBacktickException.java @@ -0,0 +1,28 @@ +/* + * 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.hadoop.hive.ql.parse; + +public class InvalidBacktickException extends SemanticException { + + private static final long serialVersionUID = 1L; + + public InvalidBacktickException(String message) { + super(message); + } + +} diff --git a/ql/src/java/org/apache/hadoop/hive/ql/parse/ObjectIdentifierHolder.java b/ql/src/java/org/apache/hadoop/hive/ql/parse/ObjectIdentifierHolder.java new file mode 100644 index 0000000000..948fe8cd5c --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/ObjectIdentifierHolder.java @@ -0,0 +1,49 @@ +/* + * 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.hadoop.hive.ql.parse; + +import java.util.Objects; +import java.util.Optional; + +/** + * An immutable container for a parent and child object identifier. The parent + * identifier is an optional value. + * + * This class is not public. Instances can be created using the + * {@link ObjectIdentifierParser#id(String, String)} factory method. + */ +final class ObjectIdentifierHolder implements ObjectIdentifierParser.ObjectIdentifier { + final Optional parent; + final String id; + + ObjectIdentifierHolder(final String parent, final String id) { + this.parent = Optional.ofNullable(parent); + this.id = Objects.requireNonNull(id); + } + + @Override + public Optional getParentIdentifier() { + return this.parent; + } + + @Override + public String getIdentifier() { + return this.id; + } + +} diff --git a/ql/src/java/org/apache/hadoop/hive/ql/parse/ObjectIdentifierParser.java b/ql/src/java/org/apache/hadoop/hive/ql/parse/ObjectIdentifierParser.java new file mode 100644 index 0000000000..eb85c0c9c5 --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/ObjectIdentifierParser.java @@ -0,0 +1,193 @@ +/* + * 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.hadoop.hive.ql.parse; + +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.hadoop.hive.ql.ErrorMsg; + +/** + * Utility class to parse MySQL-style object identifiers, including database, + * table, column, alias, view, partition, etc. + * + * @see https://dev.mysql.com/doc/refman/8.0/en/identifiers.html + */ +public final class ObjectIdentifierParser { + + /** + * Permitted characters in quoted identifiers include the full Unicode Basic. + */ + private static final Pattern QUOTED_OBJ_ID = Pattern.compile("`(.*)`"); + + /** + * Permitted characters in unquoted identifiers include: [0-9,a-z,A-Z$_] + * (basic Latin letters, digits 0-9, dollar, underscore). In Java, + * + *
+   * \w = A word character: [a-zA-Z_0-9]
+   * 
+ */ + private static final Pattern UNQUOTED_OBJ_ID = Pattern.compile("([\\w\\$]*)"); + + /** + * Utility class. May not be instantiated. + */ + private ObjectIdentifierParser() { + } + + /** + * Parse a {@code ASTNode} which has a type of {@code TOK_TABNAME}. + * + * @param node The node to parse + * @return An ObjectIdentifier container containing the (optional) database + * and table name + * @throws SemanticException if an object identifier cannot be parsed from + * this {@code ASTNode} + */ + public static ObjectIdentifier parseTableIdentifer(final ASTNode node) throws SemanticException { + if (node.getType() != HiveParser.TOK_TABNAME) { + throw new SemanticException(ASTErrorUtils.getMsg(ErrorMsg.INVALID_TABLE_NAME.getMsg(), node)); + } + return parseObjectIdenifier(node); + } + + /** + * Parse a {@code ASTNode} which has one or two children + * + * @param node The node to parse + * @return An ObjectIdentifier container containing the (optional) parent and + * child object identifiers + * @throws SemanticException if an object identifier cannot be parsed from + * this {@code ASTNode} + */ + public static ObjectIdentifier parseObjectIdenifier(final ASTNode node) throws SemanticException { + switch (node.getChildCount()) { + case 1: + return getObjectIdenifier(node.getChild(0).getText()); + case 2: + return getObjectIdenifier(node.getChild(0).getText(), node.getChild(1).getText()); + default: + throw new SemanticException(ASTErrorUtils.getMsg(ErrorMsg.INVALID_TABLE_NAME.getMsg(), node)); + } + } + + /** + * Parse the parent and child object identifiers as represented by strings. + * + * @param parentId The parent's object identifier + * @param childId The child object identifier + * @return An ObjectIdentifier container containing the (optional) parent and + * child object identifiers + * @throws SemanticException if an object identifier cannot be parsed from + * this parent and child identifier + * @throws NullPointerExceptionif parentId or childId is {@code null} + */ + public static ObjectIdentifier getObjectIdenifier(String parentId, String childId) throws SemanticException { + return id(parseObjectIdenifier(parentId), parseObjectIdenifier(childId)); + } + + /** + * Parse a child object identifier represented as a string. + * + * @param parentId The parent's object identifier (may be null) + * @param childId The child object identifier + * @return An ObjectIdentifier container containing the (optional) parent and + * child object identifiers + * @throws SemanticException if an object identifier cannot be parsed from + * this child identifier + * @throws NullPointerExceptionif childId is {@code null} + */ + public static ObjectIdentifier getObjectIdenifier(String childId) throws SemanticException { + return id(null, parseObjectIdenifier(childId)); + } + + /** + * Parse a single identifier. Any backticks surrounding the identifier are + * stripped and the raw version is returned. The raw identifier is also tested + * against several validation rules to ensure it complies with MySQL + * standards. + * + * @param id A object identifier + * @return The parsed version of the object identifier + * @throws SemanticException if an object identifier cannot be parsed from + * this child identifier + * @throws NullPointerExceptionif childId is {@code null} + */ + public static String parseObjectIdenifier(final String id) throws SemanticException { + Objects.requireNonNull(id); + final Matcher matcherQuoted = QUOTED_OBJ_ID.matcher(id); + if (matcherQuoted.matches()) { + return validateObjectIdentifier(matcherQuoted.group(1), true); + } + + final Matcher matcherUnquoted = UNQUOTED_OBJ_ID.matcher(id); + if (matcherUnquoted.matches()) { + return validateObjectIdentifier(matcherUnquoted.group(1), false); + } + + throw new InvalidBacktickException("Improperly quoted table name: " + id); + } + + /** + * Validate that the object identifier adheres to some special case rules. + * + * @param oid The object identifier (backticks are stripped) + * @param quoted If the identifier was originally enclosed in backticks + * @return The object identifier passed in + * @throws SemanticException If the identifier failed any validations + */ + private static String validateObjectIdentifier(final String oid, final boolean quoted) throws SemanticException { + Objects.requireNonNull(oid); + if (oid.isEmpty()) { + throw new SemanticException("Identifier cannot be empty"); + } + if (oid.length() > 64) { + throw new ObjectIdentifierTooLong(64, oid); + } + if (oid.matches(".*\\s+")) { + throw new SemanticException("Identifier cannot end with space characters: `" + oid + "`"); + } + if (!quoted && oid.matches("\\d+")) { + throw new SemanticException( + "Identifiers may begin with a digit but only quoted identifiers may consist solely of digits: `" + oid + "`"); + } + return oid; + } + + /** + * Helper method to wrap an (optional) parent ID and object ID together. + * + * @param parentId The object's parent ID (may be null) + * @param oid The object identifier + * @return A container combining the two together. + * @throws NullPointerException if the oid is {@code null} + */ + static ObjectIdentifier id(final String parentId, final String oid) { + return new ObjectIdentifierHolder(parentId, oid); + } + + public static interface ObjectIdentifier { + Optional getParentIdentifier(); + + String getIdentifier(); + } +} diff --git a/ql/src/java/org/apache/hadoop/hive/ql/parse/ObjectIdentifierTooLong.java b/ql/src/java/org/apache/hadoop/hive/ql/parse/ObjectIdentifierTooLong.java new file mode 100644 index 0000000000..d67f045718 --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/ObjectIdentifierTooLong.java @@ -0,0 +1,28 @@ +/* + * 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.hadoop.hive.ql.parse; + +public class ObjectIdentifierTooLong extends SemanticException { + + private static final long serialVersionUID = 1L; + + public ObjectIdentifierTooLong(int maxLength, String oid) { + super(String.format("Identifier is too long [max:%d]: `%s`", maxLength, oid)); + } + +} diff --git a/ql/src/test/org/apache/hadoop/hive/ql/parse/TestObjectIdentifierParser.java b/ql/src/test/org/apache/hadoop/hive/ql/parse/TestObjectIdentifierParser.java new file mode 100644 index 0000000000..40c582e7de --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/parse/TestObjectIdentifierParser.java @@ -0,0 +1,303 @@ +/* + * 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.hadoop.hive.ql.parse; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; + +import org.antlr.runtime.Token; +import org.antlr.runtime.tree.Tree; +import org.apache.hadoop.hive.ql.parse.ObjectIdentifierParser.ObjectIdentifier; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** + * Test the ObjectIdentifierParser class. + */ +public class TestObjectIdentifierParser { + + @Mock + ASTNode rootNode; + + @Mock + Tree nodeChildZero; + + @Mock + Tree nodeChildOne; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Test(expected = SemanticException.class) + public void testNonTableToken() throws SemanticException { + final Token mockToken = mock(Token.class); + when(mockToken.getLine()).thenReturn(0); + + when(rootNode.getType()).thenReturn(HiveParser.TOK_DROPDATABASE); + when(rootNode.getToken()).thenReturn(mockToken); + ObjectIdentifierParser.parseTableIdentifer(rootNode); + } + + @Test(expected = SemanticException.class) + public void testInvalidChildCount() throws SemanticException { + final Token mockToken = mock(Token.class); + when(mockToken.getLine()).thenReturn(0); + + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(3).thenReturn(0); + when(rootNode.getToken()).thenReturn(mockToken); + + ObjectIdentifierParser.parseTableIdentifer(rootNode); + } + + @Test(expected = InvalidBacktickException.class) + public void testTableNameMissingLastQuote() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn("`table"); + ObjectIdentifierParser.parseTableIdentifer(rootNode); + } + + @Test(expected = InvalidBacktickException.class) + public void testTableNameMissingFirstQuote() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn("table`"); + ObjectIdentifierParser.parseTableIdentifer(rootNode); + } + + /** + * Tests a scenario where a statement uses single-quote (') for the table + * name. This will be treated as if the table name has not been quoted at all + * (since backticks are the only valid wrappers). The single-quote charater is + * not allowed when the table name is not wrapped with backticks. + */ + @Test(expected = InvalidBacktickException.class) + public void testTableNameSingleQuote() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn("'table'"); + ObjectIdentifierParser.parseTableIdentifer(rootNode); + } + + @Test(expected = SemanticException.class) + public void testTableNameUnquotedEmpty() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn(""); + ObjectIdentifierParser.parseTableIdentifer(rootNode); + } + + @Test(expected = SemanticException.class) + public void testTableNameQuotedEmpty() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn("``"); + ObjectIdentifierParser.parseTableIdentifer(rootNode); + } + + @Test(expected = SemanticException.class) + public void testTableNameQuotedEndsWithSpace() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn("`table `"); + ObjectIdentifierParser.parseTableIdentifer(rootNode); + } + + /** + * The maximum object identifier length for most objects is 64. + */ + @Test(expected = ObjectIdentifierTooLong.class) + public void testTableNameTooLong() throws SemanticException { + final char[] longTableName = new char[65]; + Arrays.fill(longTableName, 'f'); + final String testId = new String(longTableName); + + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn(testId); + ObjectIdentifierParser.parseTableIdentifer(rootNode); + } + + /** + * Identifiers may begin with a digit but only quoted identifiers may consist + * solely of digits. + */ + @Test(expected = SemanticException.class) + public void testChildUnquotedNumberical() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn("12345678"); + + ObjectIdentifierParser.parseTableIdentifer(rootNode); + } + + /** + * Identifiers may begin with a digit but only quoted identifiers may consist + * solely of digits. + */ + @Test + public void testChildQuotedNumberical() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn("`12345678`"); + + ObjectIdentifier oid = ObjectIdentifierParser.parseTableIdentifer(rootNode); + assertFalse(oid.getParentIdentifier().isPresent()); + assertEquals("12345678", oid.getIdentifier()); + } + + @Test(expected = InvalidBacktickException.class) + public void testTableNameDoubleQuote() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn("\"table\""); + ObjectIdentifierParser.parseTableIdentifer(rootNode); + } + + @Test + public void testTableNameUnquoted() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn("testTable"); + ObjectIdentifier oid = ObjectIdentifierParser.parseTableIdentifer(rootNode); + assertFalse(oid.getParentIdentifier().isPresent()); + assertEquals("testTable", oid.getIdentifier()); + } + + @Test + public void testTableNameQuoted() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn("`testTable`"); + ObjectIdentifier oid = ObjectIdentifierParser.parseTableIdentifer(rootNode); + assertFalse(oid.getParentIdentifier().isPresent()); + assertEquals("testTable", oid.getIdentifier()); + } + + /** + * Test the longest allowable object identifier (max: 64). + */ + @Test + public void testTableNameQuotedLong() throws SemanticException { + final char[] longTableName = new char[64]; + Arrays.fill(longTableName, 'f'); + final String testId = new String(longTableName); + + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn(testId); + ObjectIdentifier oid = ObjectIdentifierParser.parseTableIdentifer(rootNode); + assertFalse(oid.getParentIdentifier().isPresent()); + assertEquals(testId, oid.getIdentifier()); + } + + @Test + public void testParentUnquotedChildUnquoted() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(2); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(rootNode.getChild(1)).thenReturn(nodeChildOne); + when(nodeChildZero.getText()).thenReturn("mydatabase"); + when(nodeChildOne.getText()).thenReturn("mytable"); + + ObjectIdentifier oid = ObjectIdentifierParser.parseTableIdentifer(rootNode); + assertEquals("mydatabase", oid.getParentIdentifier().get()); + assertEquals("mytable", oid.getIdentifier()); + } + + @Test + public void testParentQuotedChildUnquoted() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(2); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(rootNode.getChild(1)).thenReturn(nodeChildOne); + when(nodeChildZero.getText()).thenReturn("mydatabase"); + when(nodeChildOne.getText()).thenReturn("`mytable`"); + + ObjectIdentifier oid = ObjectIdentifierParser.parseTableIdentifer(rootNode); + assertEquals("mydatabase", oid.getParentIdentifier().get()); + assertEquals("mytable", oid.getIdentifier()); + } + + @Test + public void testParentUnquotedChildQuoted() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(2); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(rootNode.getChild(1)).thenReturn(nodeChildOne); + when(nodeChildZero.getText()).thenReturn("mydatabase"); + when(nodeChildOne.getText()).thenReturn("`mytable`"); + + ObjectIdentifier oid = ObjectIdentifierParser.parseTableIdentifer(rootNode); + assertEquals("mydatabase", oid.getParentIdentifier().get()); + assertEquals("mytable", oid.getIdentifier()); + } + + @Test + public void testParentQuotedChildQuoted() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(2); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(rootNode.getChild(1)).thenReturn(nodeChildOne); + when(nodeChildZero.getText()).thenReturn("`mydatabase`"); + when(nodeChildOne.getText()).thenReturn("`mytable`"); + + ObjectIdentifier oid = ObjectIdentifierParser.parseTableIdentifer(rootNode); + assertEquals("mydatabase", oid.getParentIdentifier().get()); + assertEquals("mytable", oid.getIdentifier()); + } + + /** + * This is a common mistake. Quoting the entire string treats this as a single + * child object where the entire string between the backticks is considered. + * It is valid to do this but ill advised. + */ + @Test + public void testParentDotChildQuoted() throws SemanticException { + when(rootNode.getType()).thenReturn(HiveParser.TOK_TABNAME); + when(rootNode.getChildCount()).thenReturn(1); + when(rootNode.getChild(0)).thenReturn(nodeChildZero); + when(nodeChildZero.getText()).thenReturn("`mydatabase.mytable`"); + + ObjectIdentifier oid = ObjectIdentifierParser.parseTableIdentifer(rootNode); + assertFalse(oid.getParentIdentifier().isPresent()); + assertEquals("mydatabase.mytable", oid.getIdentifier()); + } + +}