/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with VoltDB. If not, see <http://www.gnu.org/licenses/>. */ package org.voltdb.expressions; import org.json_voltpatches.JSONException; import org.json_voltpatches.JSONObject; import org.json_voltpatches.JSONStringer; import org.voltdb.VoltType; import org.voltdb.catalog.Table; import org.voltdb.types.ExpressionType; public class FunctionExpression extends AbstractExpression { private enum Members { NAME, IMPLIED_ARGUMENT, FUNCTION_ID, RESULT_TYPE_PARAM_IDX, } private final static int NOT_PARAMETERIZED = -1; /// The name of the actual generic SQL function being invoked, /// normally this is just the upper-case formatted version of the function /// name that was used in the SQL statement. /// It can be something different when the statement used a function name /// that is just an alias or a specialization of another function. /// For example: /// Name used in SQL | m_name /// ABS | ABS /// NOW | CURRENT_TIMESTAMP /// DAY | EXTRACT /// WEEK | EXTRACT /// EXTRACT | EXTRACT /// LTRIM | TRIM /// TRIM | TRIM private String m_name; /// An optional implied keyword argument, always upper case, /// normally null -- except for functions that had an initial keyword /// argument which got optimized out of its argument list prior to export /// from the HSQL front-end. /// For example: /// SQL invocation | m_impliedArgument /// ABS | null /// NOW | null /// DAY | DAY -- because of aliasing to EXTRACT(DAY... /// WEEK | WEEK -- because of aliasing to EXTRACT(WEEK... /// EXTRACT(DAY... | DAY /// EXTRACT(week... | WEEK /// LTRIM | LEADING /// TRIM(LEADING... | LEADING private String m_impliedArgument; /// The unique function (implementation) identifier for a named SQL /// function, assigned from constants defined in various HSQL frontend /// modules like Tokens.java, FunctionSQL.java, FunctionForVoltDB.java, /// etc. AND identically defined in functionsexpression.h. /// Aliases for the same function (implementation) will share an ID. /// For example, NOW and CURRENT_TIMESTAMP share one. /// Functions whose behavior is parameterized by a keyword argument will /// have a different function for each possible value of that argument /// For example, EXTRACT(DAY... and EXTRACT(WEEK... will have different IDs, /// which they share, respectively, with their aliases DAY(... and WEEK(... private int m_functionId; /// Defaults to the out-of-range value NOT_PARAMETERIZED for normal functions /// that have a fixed return type. For functions with a return type that depends /// on the type of one of its parameters, this is the index of that parameter. /// For example, 1 for the MOD function, 0 for ABS, etc. private int m_resultTypeParameterIndex = NOT_PARAMETERIZED; /// This is used for both initial construction /// and JSON deserialization. public FunctionExpression() { super(); setExpressionType(ExpressionType.FUNCTION); } public void setAttributes(String name, String impliedArgument, int id) { assert(name != null); m_name = name; m_impliedArgument = impliedArgument; m_functionId = id; } public boolean hasFunctionId(int functionId) { return m_functionId == functionId; } public void setResultTypeParameterIndex(int resultTypeParameterIndex) { m_resultTypeParameterIndex = resultTypeParameterIndex; } /** Negotiate the type(s) of the parameterized function's result and its parameter argument. * This avoids a fatal "non-castable type" runtime exception. */ public void negotiateInitialValueTypes() { // Either of the function result type or parameter type could be null or a specific supported value type, or a generic // NUMERIC. Replace any "generic" type (null or NUMERIC) with the more specific type without over-specifying // -- the BEST type might only become clear later when the context/caller of this function is parsed, so don't // risk guessing wrong here just for the sake of specificity. // There will be a "finalize" pass over the completed expression tree to finish specifying any remaining "generics". // DO use the type chosen by HSQL for the parameterized function as a specific type hint // for numeric constant arguments that could either go decimal or float. AbstractExpression typing_arg = m_args.get(m_resultTypeParameterIndex); VoltType param_type = typing_arg.getValueType(); VoltType value_type = getValueType(); // The heuristic for which type to change is that any type (parameter type or return type) specified so far, // including NUMERIC is better than nothing. And that anything else is better than NUMERIC. if (value_type != param_type) { if (value_type == null) { value_type = param_type; } else if (value_type == VoltType.NUMERIC) { if (param_type != null) { value_type = param_type; } // Pushing a type DOWN to the argument is a lot like work, and not worth it just to // propagate down a known NUMERIC return type, // since it will just have to be re-specialized when a more specific type is inferred from // the context or finalized when the expression is complete. } else if ((param_type == null) || (param_type == VoltType.NUMERIC)) { // The only purpose of refining the parameter argument's type is to force a more specific // refinement than NUMERIC as implied by HSQL, in case that might be more specific than // what can be inferred later from the function call context. typing_arg.refineValueType(value_type, value_type.getMaxLengthInBytes()); } } if (value_type != null) { setValueType(value_type); if (value_type != VoltType.INVALID && value_type != VoltType.NUMERIC) { int size = value_type.getMaxLengthInBytes(); setValueSize(size); } } } @Override public void validate() throws Exception { super.validate(); // // Validate that there are no children other than the argument list (mandatory even if empty) // if (m_left != null) { throw new Exception("ERROR: The left child expression '" + m_left + "' for '" + this + "' is not NULL"); } if (m_right != null) { throw new Exception("ERROR: The right child expression '" + m_right + "' for '" + this + "' is not NULL"); } if (m_args == null) { throw new Exception("ERROR: The function argument list for '" + this + "' is NULL"); } if (m_name == null) { throw new Exception("ERROR: The function name for '" + this + "' is NULL"); } if (m_resultTypeParameterIndex != NOT_PARAMETERIZED) { if (m_resultTypeParameterIndex < 0 || m_resultTypeParameterIndex >= m_args.size()) { throw new Exception("ERROR: The function parameter argument index '" + m_resultTypeParameterIndex + "' for '" + this + "' is out of bounds"); } } } @Override public boolean hasEqualAttributes(AbstractExpression obj) { if (obj instanceof FunctionExpression == false) { return false; } FunctionExpression expr = (FunctionExpression) obj; // Function id determines all other attributes return m_functionId == expr.m_functionId; } @Override public int hashCode() { // based on implementation of equals int result = m_functionId; // defer to the superclass, which factors in arguments and other attributes return result += super.hashCode(); } @Override public void toJSONString(JSONStringer stringer) throws JSONException { super.toJSONString(stringer); assert(m_name != null); stringer.keySymbolValuePair(Members.NAME.name(), m_name); stringer.keySymbolValuePair(Members.FUNCTION_ID.name(), m_functionId); if (m_impliedArgument != null) { stringer.keySymbolValuePair(Members.IMPLIED_ARGUMENT.name(), m_impliedArgument); } if (m_resultTypeParameterIndex != NOT_PARAMETERIZED) { stringer.keySymbolValuePair(Members.RESULT_TYPE_PARAM_IDX.name(), m_resultTypeParameterIndex); } } @Override protected void loadFromJSONObject(JSONObject obj) throws JSONException { m_name = obj.getString(Members.NAME.name()); assert(m_name != null); m_functionId = obj.getInt(Members.FUNCTION_ID.name()); m_impliedArgument = null; if (obj.has(Members.IMPLIED_ARGUMENT.name())) { m_impliedArgument = obj.getString(Members.IMPLIED_ARGUMENT.name()); } if (obj.has(Members.RESULT_TYPE_PARAM_IDX.name())) { m_resultTypeParameterIndex = obj.getInt(Members.RESULT_TYPE_PARAM_IDX.name()); } else { m_resultTypeParameterIndex = NOT_PARAMETERIZED; } } @Override public void refineOperandType(VoltType columnType) { if (m_resultTypeParameterIndex == NOT_PARAMETERIZED) { // Non-parameterized functions should have a fixed SPECIFIC type. // Further refinement should be useless/un-possible. return; } // A parameterized function may be able to usefully refine its parameter argument's type // and have that change propagate up to its return type. if (m_valueType != null && m_valueType != VoltType.NUMERIC) { return; } AbstractExpression arg = m_args.get(m_resultTypeParameterIndex); VoltType valueType = arg.getValueType(); if (valueType != null && valueType != VoltType.NUMERIC) { return; } arg.refineOperandType(columnType); m_valueType = arg.getValueType(); m_valueSize = m_valueType.getLengthInBytesForFixedTypes(); } @Override public void refineValueType(VoltType neededType, int neededSize) { if (m_resultTypeParameterIndex == NOT_PARAMETERIZED) { // Non-parameterized functions should have a fixed SPECIFIC type. // Further refinement should be useless/un-possible. return; } // A parameterized function may be able to usefully refine its parameter argument's type // and have that change propagate up to its return type. if (m_valueType != null && m_valueType != VoltType.NUMERIC) { return; } AbstractExpression arg = m_args.get(m_resultTypeParameterIndex); VoltType valueType = arg.getValueType(); if (valueType != null && valueType != VoltType.NUMERIC) { return; } // No assumption is made that functions that are parameterized by // variably-sized types are size-preserving, so allow any size arg.refineValueType(neededType, neededType.getMaxLengthInBytes()); m_valueType = arg.getValueType(); m_valueSize = m_valueType.getMaxLengthInBytes(); } @Override public void finalizeValueTypes() { finalizeChildValueTypes(); if (m_resultTypeParameterIndex == NOT_PARAMETERIZED) { // Non-parameterized functions should have a fixed SPECIFIC type. // Further refinement should be useless/un-possible. return; } // A parameterized function should reflect the final value of its parameter argument's type. AbstractExpression arg = m_args.get(m_resultTypeParameterIndex); m_valueType = arg.getValueType(); m_valueSize = m_valueType.getMaxLengthInBytes(); } @Override public void resolveForTable(Table table) { resolveChildrenForTable(table); if (m_resultTypeParameterIndex == NOT_PARAMETERIZED) { // Non-parameterized functions should have a fixed SPECIFIC type. // Further refinement should be useless/un-possible. return; } // resolving a child column has type implications for parameterized functions negotiateInitialValueTypes(); } @Override public String explain(String impliedTableName) { assert(m_name != null); String result = m_name; if ( ! m_args.isEmpty()) { String connector = "("; // The purpose of m_impliedArgument is to allow functions with // different leading keyword arguments to be implemented as different // functions in VoltDB but to be "unified" back to their // original/generic form when explained to the user or // re-generated as SQL syntax for round trips back to the parser. // For example, SQL function invocations like // "trim(leading 'X' from field)" and // "trim(trailing 'X' from field)" // get invoked as separate volt functions. // They are modeled internally as something more like // "trim_leading('X', field)" and "trim_trailing('X', field)". // Note: it's actually m_functionId, not m_impliedParameter that // drives that distinction. // We slightly extended the supported SQL grammar to allow consistent // use of comma separators in the place of keyword separators, // even in non-traditional cases like // "trim(leading, 'X', field)" // as a normalized equivalent of // "trim(leading 'X' from field)". // SQL functions that use this mechanism include variants of // "extract", "since_epoch", "to_timestamp", "trim" and "truncate" // and their various aliases. // It is assumed that there is at least 1 explicit argument following // the implied argument (so m_args will not be empty for these cases). if (m_impliedArgument != null) { result += connector + m_impliedArgument; connector = ", "; } // Append each normal argument. for (AbstractExpression arg : m_args) { result += connector + arg.explain(impliedTableName); connector = ", "; } result += ")"; } else { // The two functions MIN_VALID_TIMESTAMP and MAX_VALID_TIMESTAMP // are nullary. Others may be in the future. result += "()"; } return result; } @Override public boolean isValueTypeIndexable(StringBuffer msg) { StringBuffer dummyMsg = new StringBuffer(); if (!super.isValueTypeIndexable(dummyMsg)) { msg.append("a " + m_valueType.getName() + " valued function '"+ m_name.toUpperCase() + "'"); return false; } return true; } @Override public void findUnsafeOperatorsForDDL(UnsafeOperatorsForDDL ops) { ops.add(explain("Be Explicit")); } }