Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@RequiredArgsConstructor
public class Convert extends UnresolvedPlan {
private final List<Let> conversions;
private final String timeFormat;
private UnresolvedPlan child;

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ public RelNode visitConvert(Convert node, CalcitePlanContext context) {
ConversionState state = new ConversionState();

for (Let conversion : node.getConversions()) {
processConversion(conversion, state, context);
processConversion(conversion, node.getTimeFormat(), state, context);
}

return buildConversionProjection(state, context);
Expand All @@ -1021,14 +1021,14 @@ private static class ConversionState {
}

private void processConversion(
Let conversion, ConversionState state, CalcitePlanContext context) {
Let conversion, String timeFormat, ConversionState state, CalcitePlanContext context) {
String target = conversion.getVar().getField().toString();
UnresolvedExpression expression = conversion.getExpression();

if (expression instanceof Field) {
processFieldCopyConversion(target, (Field) expression, state, context);
} else if (expression instanceof Function) {
processFunctionConversion(target, (Function) expression, state, context);
processFunctionConversion(target, (Function) expression, timeFormat, state, context);
} else {
throw new SemanticCheckException("Convert command requires function call expressions");
}
Expand All @@ -1051,7 +1051,11 @@ private void processFieldCopyConversion(
}

private void processFunctionConversion(
String target, Function function, ConversionState state, CalcitePlanContext context) {
String target,
Function function,
String timeFormat,
ConversionState state,
CalcitePlanContext context) {
String functionName = function.getFuncName();
List<UnresolvedExpression> args = function.getFuncArgs();

Expand All @@ -1068,8 +1072,7 @@ private void processFunctionConversion(
state.seenFields.add(source);

RexNode sourceField = context.relBuilder.field(source);
RexNode convertCall =
PPLFuncImpTable.INSTANCE.resolve(context.rexBuilder, functionName, sourceField);
RexNode convertCall = resolveConvertFunction(functionName, sourceField, timeFormat, context);

if (!target.equals(source)) {
state.additions.add(Pair.of(target, context.relBuilder.alias(convertCall, target)));
Expand All @@ -1078,6 +1081,23 @@ private void processFunctionConversion(
}
}

private RexNode resolveConvertFunction(
String functionName, RexNode sourceField, String timeFormat, CalcitePlanContext context) {

// Time functions that support timeformat parameter
Set<String> timeFunctions = Set.of("ctime", "mktime");

if (timeFunctions.contains(functionName.toLowerCase()) && timeFormat != null) {
// For time functions with custom timeformat, pass the format as a second parameter
RexNode timeFormatLiteral = context.rexBuilder.makeLiteral(timeFormat);
return PPLFuncImpTable.INSTANCE.resolve(
context.rexBuilder, functionName, sourceField, timeFormatLiteral);
} else {
// Regular conversion functions or time functions without custom format
return PPLFuncImpTable.INSTANCE.resolve(context.rexBuilder, functionName, sourceField);
}
}

private RelNode buildConversionProjection(ConversionState state, CalcitePlanContext context) {
List<String> originalFields = context.relBuilder.peek().getRowType().getFieldNames();
List<RexNode> projectList = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ private PPLOperandTypes() {}
UDFOperandMetadata.wrap(
(CompositeOperandTypeChecker)
OperandTypes.ANY.or(OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.INTEGER)));
public static final UDFOperandMetadata ANY_OPTIONAL_STRING =
UDFOperandMetadata.wrap(
(CompositeOperandTypeChecker)
OperandTypes.ANY.or(OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.CHARACTER)));
public static final UDFOperandMetadata ANY_OPTIONAL_TIMESTAMP =
UDFOperandMetadata.wrap(
(CompositeOperandTypeChecker)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,4 +249,40 @@ private static long extractFirstNDigits(double value, int digits) {

return isNegative ? -result : result;
}

/** Mapping from strftime specifiers to Java DateTimeFormatter patterns for parsing. */
private static final Map<String, String> STRFTIME_TO_JAVA_PARSE =
ImmutableMap.<String, String>builder()
.put("%Y", "yyyy")
.put("%y", "yy")
.put("%m", "MM")
.put("%B", "MMMM")
.put("%b", "MMM")
.put("%d", "dd")
.put("%H", "HH")
.put("%I", "hh")
.put("%M", "mm")
.put("%S", "ss")
.put("%p", "a")
.put("%T", "HH:mm:ss")
.put("%F", "yyyy-MM-dd")
.put("%%", "'%'")
.build();

/**
* Convert a strftime format string to a Java DateTimeFormatter pattern suitable for parsing.
*
* @param strftimeFormat the strftime-style format string (e.g. {@code %Y-%m-%d %H:%M:%S})
* @return a Java DateTimeFormatter pattern (e.g. {@code yyyy-MM-dd HH:mm:ss})
*/
public static String toJavaPattern(String strftimeFormat) {
Matcher m = Pattern.compile("%[A-Za-z%]").matcher(strftimeFormat);
StringBuilder sb = new StringBuilder();
while (m.find()) {
String replacement = STRFTIME_TO_JAVA_PARSE.getOrDefault(m.group(), m.group());
m.appendReplacement(sb, Matcher.quoteReplacement(replacement));
}
m.appendTail(sb);
return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,12 @@
import org.opensearch.sql.expression.function.jsonUDF.JsonKeysFunctionImpl;
import org.opensearch.sql.expression.function.jsonUDF.JsonSetFunctionImpl;
import org.opensearch.sql.expression.function.udf.AutoConvertFunction;
import org.opensearch.sql.expression.function.udf.CTimeConvertFunction;
import org.opensearch.sql.expression.function.udf.CryptographicFunction;
import org.opensearch.sql.expression.function.udf.Dur2SecConvertFunction;
import org.opensearch.sql.expression.function.udf.MemkConvertFunction;
import org.opensearch.sql.expression.function.udf.MkTimeConvertFunction;
import org.opensearch.sql.expression.function.udf.MsTimeConvertFunction;
import org.opensearch.sql.expression.function.udf.NumConvertFunction;
import org.opensearch.sql.expression.function.udf.ParseFunction;
import org.opensearch.sql.expression.function.udf.RelevanceQueryFunction;
Expand Down Expand Up @@ -431,6 +435,10 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable {
public static final SqlOperator RMCOMMA = new RmcommaConvertFunction().toUDF("RMCOMMA");
public static final SqlOperator RMUNIT = new RmunitConvertFunction().toUDF("RMUNIT");
public static final SqlOperator MEMK = new MemkConvertFunction().toUDF("MEMK");
public static final SqlOperator CTIME = new CTimeConvertFunction().toUDF("CTIME");
public static final SqlOperator MKTIME = new MkTimeConvertFunction().toUDF("MKTIME");
public static final SqlOperator MSTIME = new MsTimeConvertFunction().toUDF("MSTIME");
public static final SqlOperator DUR2SEC = new Dur2SecConvertFunction().toUDF("DUR2SEC");

public static final SqlOperator WIDTH_BUCKET =
new org.opensearch.sql.expression.function.udf.binning.WidthBucketFunction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.COT;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.COUNT;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CRC32;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CTIME;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CURDATE;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CURRENT_DATE;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CURRENT_TIME;
Expand All @@ -61,6 +62,7 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.DEGREES;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.DIVIDE;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.DIVIDEFUNCTION;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.DUR2SEC;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.E;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.EARLIEST;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.EQUAL;
Expand Down Expand Up @@ -144,12 +146,14 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MINUTE;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MINUTE_OF_DAY;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MINUTE_OF_HOUR;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MKTIME;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MOD;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MODULUS;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MODULUSFUNCTION;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MONTH;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MONTHNAME;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MONTH_OF_YEAR;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MSTIME;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MULTIPLY;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MULTIPLYFUNCTION;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.MULTI_MATCH;
Expand Down Expand Up @@ -995,6 +999,10 @@ void populate() {
registerOperator(RMCOMMA, PPLBuiltinOperators.RMCOMMA);
registerOperator(RMUNIT, PPLBuiltinOperators.RMUNIT);
registerOperator(MEMK, PPLBuiltinOperators.MEMK);
registerOperator(CTIME, PPLBuiltinOperators.CTIME);
registerOperator(MKTIME, PPLBuiltinOperators.MKTIME);
registerOperator(MSTIME, PPLBuiltinOperators.MSTIME);
registerOperator(DUR2SEC, PPLBuiltinOperators.DUR2SEC);
Comment on lines +1002 to +1005
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these have to a UDF?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I couldn't find any Calcite operators that handled parsing time the formats that was needed for these functions


register(
TOSTRING,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.expression.function.udf;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
import org.apache.calcite.adapter.enumerable.NotNullImplementor;
import org.apache.calcite.adapter.enumerable.NullPolicy;
import org.apache.calcite.adapter.enumerable.RexToLixTranslator;
import org.apache.calcite.linq4j.tree.Expression;
import org.apache.calcite.linq4j.tree.Expressions;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.sql.type.SqlReturnTypeInference;
import org.opensearch.sql.calcite.utils.PPLOperandTypes;
import org.opensearch.sql.calcite.utils.PPLReturnTypes;
import org.opensearch.sql.expression.datetime.StrftimeFormatterUtil;
import org.opensearch.sql.expression.function.ImplementorUDF;
import org.opensearch.sql.expression.function.UDFOperandMetadata;

/**
* PPL ctime() conversion function. Converts UNIX epoch timestamps to human-readable time strings
* using strftime format specifiers. Default format: {@code %m/%d/%Y %H:%M:%S}.
*/
public class CTimeConvertFunction extends ImplementorUDF {

private static final String DEFAULT_FORMAT = "%m/%d/%Y %H:%M:%S";

public CTimeConvertFunction() {
super(new CTimeImplementor(), NullPolicy.ANY);
}

@Override
public SqlReturnTypeInference getReturnTypeInference() {
return PPLReturnTypes.STRING_FORCE_NULLABLE;
}

@Override
public UDFOperandMetadata getOperandMetadata() {
return PPLOperandTypes.ANY_OPTIONAL_STRING;
}

public static class CTimeImplementor implements NotNullImplementor {
@Override
public Expression implement(
RexToLixTranslator translator, RexCall call, List<Expression> translatedOperands) {
if (translatedOperands.isEmpty()) {
return Expressions.constant(null, String.class);
}
Expression fieldValue = Expressions.box(translatedOperands.get(0));
if (translatedOperands.size() == 1) {
return Expressions.call(CTimeConvertFunction.class, "convert", fieldValue);
}
Expression timeFormat = Expressions.box(translatedOperands.get(1));
return Expressions.call(
CTimeConvertFunction.class, "convertWithFormat", fieldValue, timeFormat);
}
}

public static String convert(Object value) {
return convertWithFormat(value, null);
}

public static String convertWithFormat(Object value, Object timeFormatObj) {
Double timestamp = toEpochSeconds(value);
if (timestamp == null) {
return null;
}
String format = (timeFormatObj != null) ? timeFormatObj.toString().trim() : DEFAULT_FORMAT;
if (format.isEmpty()) {
return null;
}
try {
long seconds = timestamp.longValue();
int nanos = (int) ((timestamp - seconds) * 1_000_000_000);
Instant instant = Instant.ofEpochSecond(seconds, nanos);
ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"));
return StrftimeFormatterUtil.formatZonedDateTime(zdt, format).stringValue();
} catch (Exception e) {
return null;
}
}

public static Double toEpochSeconds(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
String str = value.toString().trim();
if (str.isEmpty()) {
return null;
}
try {
return Double.parseDouble(str);
} catch (NumberFormatException e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.expression.function.udf;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** PPL dur2sec() conversion function. Converts duration format {@code [D+]HH:MM:SS} to seconds */
public class Dur2SecConvertFunction extends BaseConversionUDF {

public static final Dur2SecConvertFunction INSTANCE = new Dur2SecConvertFunction();

// Matches [D+]HH:MM:SS — optional days prefix with + separator
private static final Pattern DURATION_PATTERN =
Pattern.compile("^(?:(\\d+)\\+)?(\\d{1,2}):(\\d{1,2}):(\\d{1,2})$");

public Dur2SecConvertFunction() {
super(Dur2SecConvertFunction.class);
}

public static Object convert(Object value) {
return INSTANCE.convertValue(value);
}

@Override
protected Object applyConversion(String preprocessedValue) {
Double existingSeconds = tryParseDouble(preprocessedValue);
if (existingSeconds != null) {
return existingSeconds;
}

Matcher matcher = DURATION_PATTERN.matcher(preprocessedValue);
if (!matcher.matches()) {
return null;
}

try {
int days = matcher.group(1) != null ? Integer.parseInt(matcher.group(1)) : 0;
int hours = Integer.parseInt(matcher.group(2));
int minutes = Integer.parseInt(matcher.group(3));
int seconds = Integer.parseInt(matcher.group(4));

if (hours >= 24 || minutes >= 60 || seconds >= 60) {
return null;
}

return (double) (days * 86400 + hours * 3600 + minutes * 60 + seconds);
} catch (NumberFormatException e) {
return null;
}
}
}
Loading
Loading