All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.axibase.tsd.driver.jdbc.converter.AtsdSqlConverter Maven / Gradle / Ivy

There is a newer version: 1.4.11
Show newest version
package com.axibase.tsd.driver.jdbc.converter;

import com.axibase.tsd.driver.jdbc.ext.AtsdMeta;
import com.axibase.tsd.driver.jdbc.logging.LoggingFacade;
import java.sql.SQLDataException;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.calcite.avatica.com.fasterxml.jackson.databind.util.ISO8601Utils;
import org.apache.calcite.avatica.util.Casing;
import org.apache.calcite.avatica.util.Quoting;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlDynamicParam;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.parser.impl.SqlParserImpl;
import org.apache.calcite.util.NlsString;
import org.apache.commons.lang3.StringUtils;

import static com.axibase.tsd.driver.jdbc.DriverConstants.DEFAULT_TABLE_NAME;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

public abstract class AtsdSqlConverter {

    protected final LoggingFacade logger = LoggingFacade.getLogger(getClass());

    private static final Pattern DATETIME_ISO_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z$");
    private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?$");
    private static final char TAGS_DELIMETER = ';';

    private static final String ENTITY = "entity";
    private static final String TIME = "time";
    private static final String DATETIME = "datetime";
    private static final String METRIC = "metric";
    protected static final String VALUE = "value";
    private static final String TEXT = "text";
    protected static final String TAGS = "tags";
    protected static final String PREFIX_TAGS = "tags.";

    protected T rootNode;
    private final boolean timestampTz;

    protected AtsdSqlConverter(boolean timestampTz) {
        this.timestampTz = timestampTz;
    }

    public String convertToCommand(String sql) throws SQLException {
        return convertToCommand(sql, null);
    }

    public String convertToCommand(String sql, List parameterValues) throws SQLException {
        logger.debug("[convertToCommand] parameterCount: {}", getSize(parameterValues));
        try {
            sql = prepareSql(sql);
        } catch (RuntimeException e) {
            throw new SQLException("SQL prepare error: " + sql, e);
        }
        this.rootNode = parseSql(sql);
        final String result = createSeriesCommand(parameterValues);
        logger.trace("[convertToCommand] result: {}", result);
        return result;
    }

    public String convertBatchToCommands(String sql, List> parameterValuesBatch) throws SQLException {
        logger.debug("[convertBatchToCommands] batchSize: {}", getSize(parameterValuesBatch));
        try {
            sql = prepareSql(sql);
        } catch (RuntimeException e) {
            throw new SQLException("SQL prepare error: " + sql, e);
        }
        this.rootNode = parseSql(sql);
        final String result =createSeriesCommandBatch(parameterValuesBatch);
        logger.trace("[convertBatchToCommands] result: {}", result);
        return result;
    }

    protected abstract String prepareSql(String sql) throws SQLException;
    protected abstract String getTargetTableName();
    protected abstract List getColumnNames();
    protected abstract List getColumnValues(List parameterValues);
    protected abstract List> getColumnValuesBatch(List> parameterValuesBatch);

    private T parseSql(String sql) throws SQLException {
        logger.debug("[parseSql]");
        try {
            SqlParser sqlParser = SqlParser.create(sql, SqlParser.configBuilder()
                    .setParserFactory(SqlParserImpl.FACTORY)
                    .setUnquotedCasing(Casing.TO_LOWER)
                    .setQuoting(Quoting.DOUBLE_QUOTE)
                    .build());
            return (T) sqlParser.parseStmt();
        } catch (SqlParseException exc) {
            throw new SQLException("Could not parse sql: " + sql, exc);
        }
    }

    private String createSeriesCommand(List parameterValues) throws SQLDataException {
        logger.debug("[createSeriesCommand]");
        final String tableName = getTargetTableName();
        final List columnNames = getColumnNames();
        logger.debug("[createSeriesCommand] tableName: {} columnCount: {}", tableName, columnNames.size());
        logger.trace("[createSeriesCommand] columnNames: {}", columnNames);
        final List values = getColumnValues(parameterValues);
        logger.trace("[createSeriesCommand] values: {}", values);
        return DEFAULT_TABLE_NAME.equals(tableName) ? composeSeriesCommand(logger, columnNames, values, timestampTz) : composeSeriesCommand(logger, tableName,
                columnNames, values, timestampTz);
    }

    private String createSeriesCommandBatch(List> parameterValueBatch) throws SQLDataException {
        logger.debug("[createSeriesCommandBatch]");
        final String tableName = getTargetTableName();
        final List columnNames = getColumnNames();
        logger.debug("[createSeriesCommandBatch] tableName: {} columnCount: {}", tableName, columnNames.size());
        logger.trace("[createSeriesCommandBatch] columnNames: {}", columnNames);
        final List> valueBatch = getColumnValuesBatch(parameterValueBatch);
        StringBuilder buffer = new StringBuilder();
        if (DEFAULT_TABLE_NAME.equals(tableName)) {
            for (List values : valueBatch) {
                buffer.append(composeSeriesCommand(logger, columnNames, values, timestampTz));
            }
        } else {
            for (List values : valueBatch) {
                buffer.append(composeSeriesCommand(logger, tableName, columnNames, values, timestampTz));
            }
        }
        return buffer.toString();
    }

    private static String composeSeriesCommand(LoggingFacade logger, final String metricName, final List columnNames, final List values,
                                               boolean timestampTz)
            throws SQLDataException {
        if (columnNames.size() != values.size()) {
            throw new IndexOutOfBoundsException(
                    String.format("Number of values [%d] does not match to number of columns [%d]",
                            values.size(), columnNames.size()));
        }
        SeriesCommand command = new SeriesCommand();
        String columnName;
        Object value;
        for (int i = 0; i columnNames, final List values, boolean timestampTz) throws
            SQLDataException {
        if (columnNames.size() != values.size()) {
            throw new IndexOutOfBoundsException(
                    String.format("Number of values [%d] does not match to number of columns [%d]",
                            values.size(), columnNames.size()));
        }
        SeriesCommand command = new SeriesCommand();
        String columnName;
        Object value;
        String metricName = null;
        Double metricValue = null;
        String metricText = null;
        for (int i = 0; i T validate(Object value, Class targetClass) throws SQLDataException {
        if (targetClass.isInstance(value)) {
            return (T) value;
        }
        throw new SQLDataException("Invalid value: " + value + ". Current type: " + value.getClass().getSimpleName()
                + ", expected type: " + targetClass.getSimpleName());
    }

    private static String validateDateTime(Object value, boolean timestampTz) throws SQLDataException {
        if (value instanceof Number) {
            return AtsdMeta.TIMESTAMP_FORMATTER.format(((Number) value).longValue());
        } else if (!(value instanceof String)) {
            throw new SQLDataException("Invalid value: " + value + ". Current type: " + value.getClass().getSimpleName()
                    + ", expected type: " + Timestamp.class.getSimpleName());
        }
        final String dateTime = value.toString();
        Matcher matcher = DATETIME_ISO_PATTERN.matcher(dateTime);
        if (matcher.matches()) {
            return dateTime;
        }
        matcher = TIMESTAMP_PATTERN.matcher(dateTime);
        if (matcher.matches()) {
            final Timestamp timestamp = Timestamp.valueOf(dateTime);
            final Calendar calendar = Calendar.getInstance();
            calendar.setTimeInMillis(timestamp.getTime());
            if (timestampTz) {
                calendar.set(Calendar.ZONE_OFFSET, TimeZone.getTimeZone("UTC").getRawOffset());
            }
            return ISO8601Utils.format(calendar.getTime(), true);
        }
        throw new SQLDataException("Invalid datetime value: " + value + ". Expected formats: yyyy-MM-dd'T'HH:mm:ss[.SSS]'Z', yyyy-MM-dd HH:mm:ss[.fffffffff]");
    }

    private static Map parseTags(String value) throws SQLDataException {
        if (StringUtils.isBlank(value)) {
            return Collections.emptyMap();
        }

        String[] tags = StringUtils.split(value, TAGS_DELIMETER);
        Map result = new LinkedHashMap<>();
        Pair nameAndValue;
        for (String tag : tags) {
            nameAndValue = parseTag(StringUtils.trim(tag));
            if (nameAndValue != null) {
                result.put(nameAndValue.getKey(), nameAndValue.getValue());
            }
        }
        return result;
    }

    private static Pair parseTag(String value) throws SQLDataException {
        if (StringUtils.isBlank(value)) {
            return null;
        }
        final int idx = value.indexOf('=');
        if (idx < 1) {
            throw new SQLDataException("Invalid tags value: " + value);
        }

        String tagName = StringUtils.trim(value.substring(0, idx));
        String tagValue = StringUtils.trim(value.substring(idx + 1));

        return StringUtils.isBlank(tagValue) ? null : new ImmutablePair<>(tagName, tagValue);
    }

    public static final class DynamicParam {

        final int index;

        private DynamicParam(int index) {
            this.index = index;
        }

        private static DynamicParam create(int index) {
            return new DynamicParam(index);
        }

    }
}