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

com.clickhouse.client.ClickHouseParameterizedQuery Maven / Gradle / Ivy

The newest version!
package com.clickhouse.client;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Map.Entry;

import com.clickhouse.data.ClickHouseChecker;
import com.clickhouse.data.ClickHouseColumn;
import com.clickhouse.data.ClickHouseUtils;
import com.clickhouse.data.ClickHouseValue;
import com.clickhouse.data.ClickHouseValues;

/**
 * A parameterized query is a parsed query with parameters being extracted for
 * substitution.
 * 

* Here parameter is define in the format of {@code :[()]}. It * starts with colon, immediately followed by name, and then optionally type * within brackets. For example: in query "select :no as no, :name(String) as * name", we have two parameters: {@code no} and {@code name}. Moreover, type of * the last parameter is {@code String}. */ public class ClickHouseParameterizedQuery implements Serializable { private static final long serialVersionUID = 8108887349618342152L; /** * A part of query. */ protected static class QueryPart implements Serializable { public final String part; public final int paramIndex; public final String paramName; public final ClickHouseColumn paramType; protected QueryPart(ClickHouseConfig config, String part, int paramIndex, String paramName, String paramType, Map map) { this.part = part; this.paramIndex = paramIndex; this.paramName = paramName != null ? paramName : String.valueOf(paramIndex); if (paramType != null) { this.paramType = ClickHouseColumn.of("", paramType); map.put(paramName, this.paramType.newValue(config)); } else { this.paramType = null; map.putIfAbsent(paramName, null); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + paramIndex; result = prime * result + ((paramName == null) ? 0 : paramName.hashCode()); result = prime * result + ((paramType == null) ? 0 : paramType.hashCode()); result = prime * result + ((part == null) ? 0 : part.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } QueryPart other = (QueryPart) obj; return paramIndex == other.paramIndex && Objects.equals(paramName, other.paramName) && Objects.equals(paramType, other.paramType) && Objects.equals(part, other.part); } } /** * Substitute named parameters in given SQL. * * @param sql SQL containing named parameters * @param params mapping between parameter name and correspoding SQL * expression(NOT raw value) * @return substituted SQL, or the given sql if one of {@code sql} and * {@code params} is null or empty */ public static String apply(String sql, Map params) { StringBuilder builder = new StringBuilder(); apply(builder, sql, params); return builder.toString(); } /** * Substitute named parameters in given SQL. * * @param builder non-null string builder * @param sql SQL containing named parameters * @param params mapping between parameter name and correspoding SQL * expression(NOT raw value) */ public static void apply(StringBuilder builder, String sql, Map params) { int len = sql == null ? 0 : sql.length(); if (len < 2 || params == null || params.isEmpty()) { builder.append(sql); return; } for (int i = 0; i < len; i++) { char ch = sql.charAt(i); if (ClickHouseUtils.isQuote(ch)) { int endIndex = ClickHouseUtils.skipQuotedString(sql, i, len, ch); builder.append(sql.substring(i, endIndex)); i = endIndex - 1; } else if (i + 1 < len) { int endIndex = i + 1; char nextCh = sql.charAt(endIndex); if (ch == '-' && nextCh == ch) { endIndex = ClickHouseUtils.skipSingleLineComment(sql, i + 2, len); builder.append(sql.substring(i, endIndex)); i = endIndex - 1; } else if (ch == '/' && nextCh == '*') { endIndex = ClickHouseUtils.skipMultiLineComment(sql, i + 2, len); builder.append(sql.substring(i, endIndex)); i = endIndex - 1; } else if (ch == ':') { if (nextCh == ch) { // skip PostgreSQL-like type conversion builder.append(ch).append(ch); i = i + 1; } else if (Character.isJavaIdentifierStart(nextCh)) { StringBuilder sb = new StringBuilder().append(nextCh); for (i = i + 2; i < len; i++) { char c = sql.charAt(i); if (c == '(') { i = ClickHouseUtils.skipBrackets(sql, i, len, c) - 1; break; } else if (Character.isJavaIdentifierPart(c)) { sb.append(c); } else { i--; break; } } builder.append(params.getOrDefault(sb.toString(), ClickHouseValues.NULL_EXPR)); } else { builder.append(ch); } } else { builder.append(ch); } } else { builder.append(ch); } } } /** * Creates an instance by parsing the given query. * * @param config non-null config * @param query non-empty SQL query * @return parameterized query */ public static ClickHouseParameterizedQuery of(ClickHouseConfig config, String query) { // cache if query.length() is greater than 1024? return new ClickHouseParameterizedQuery(config, query); } protected final ClickHouseConfig config; protected final String originalQuery; private final List parts; private final Map names; private final String lastPart; /** * Default constructor. * * @param config non-null config * @param query non-blank query */ protected ClickHouseParameterizedQuery(ClickHouseConfig config, String query) { this.config = ClickHouseChecker.nonNull(config, ClickHouseConfig.TYPE_NAME); originalQuery = ClickHouseChecker.nonBlank(query, "query"); parts = new LinkedList<>(); names = new LinkedHashMap<>(); lastPart = parse(); } /** * Adds part of query and the following parameter. * * @param part part of the query, between previous and current parameter * @param paramIndex zero-based index of the parameter * @param paramType type of the parameter, could be null */ protected void addPart(String part, int paramIndex, String paramType) { addPart(part, paramIndex, null, paramType); } /** * Adds part of query and the following parameter. * * @param part part of the query, between previous and current parameter * @param paramIndex zero-based index of the parameter * @param paramName name of the parameter, null means * {@code String.valueOf(paramIndex)} * @param paramType type of the parameter, could be null */ protected void addPart(String part, int paramIndex, String paramName, String paramType) { if (paramName == null) { paramName = String.valueOf(paramIndex); } parts.add(new QueryPart(config, part, paramIndex, paramName, paramType, names)); } /** * Gets immutable list of query parts. * * @return immutable list of query parts */ protected List getParts() { return Collections.unmodifiableList(parts); } /** * Parses the query given in constructor. * * @return remaining part(right after the last parameter) after parsing, could * be null */ protected String parse() { int paramIndex = 0; int partIndex = 0; int len = originalQuery.length(); for (int i = 0; i < len; i++) { char ch = originalQuery.charAt(i); if (ClickHouseUtils.isQuote(ch)) { i = ClickHouseUtils.skipQuotedString(originalQuery, i, len, ch) - 1; } else if (i + 1 < len) { char nextCh = originalQuery.charAt(i + 1); if (ch == '-' && nextCh == ch) { i = ClickHouseUtils.skipSingleLineComment(originalQuery, i + 2, len) - 1; } else if (ch == '/' && nextCh == '*') { i = ClickHouseUtils.skipMultiLineComment(originalQuery, i + 2, len) - 1; } else if (ch == ':') { if (nextCh == ch) { // skip PostgreSQL-like type conversion i = i + 1; } else if (Character.isJavaIdentifierStart(nextCh)) { String part = partIndex != i ? originalQuery.substring(partIndex, i) : ""; String paramName = null; String paramType = null; StringBuilder builder = new StringBuilder().append(nextCh); for (i = i + 2; i < len; i++) { char c = originalQuery.charAt(i); if (Character.isJavaIdentifierPart(c)) { builder.append(c); } else { if (c == '(') { int idx = ClickHouseUtils.skipBrackets(originalQuery, i, len, c); paramType = originalQuery.substring(i + 1, idx - 1); i = idx; } break; } } partIndex = i--; if (builder.length() > 0) { paramName = builder.toString(); if (!names.containsKey(paramName)) { paramIndex++; } } parts.add(new QueryPart(config, part, paramIndex, paramName, paramType, names)); } } } } return partIndex < len ? originalQuery.substring(partIndex, len) : null; } /** * Appends last part of the query if it exists. * * @param builder non-null string builder * @return the builder */ protected StringBuilder appendLastPartIfExists(StringBuilder builder) { if (lastPart != null) { builder.append(lastPart); } return builder; } /** * Converts given raw value to SQL expression. * * @param paramName name of the parameter * @param value raw value, could be null * @return non-null SQL expression */ protected String toSqlExpression(String paramName, Object value) { ClickHouseValue template = names.get(paramName); return template != null ? template.update(value).toSqlExpression() : ClickHouseValues.convertToSqlExpression(value); } /** * Applies stringified parameters to the given string builder. * * @param builder non-null string builder * @param params stringified parameters */ public void apply(StringBuilder builder, Map params) { if (!hasParameter()) { builder.append(originalQuery); return; } if (params == null) { params = Collections.emptyMap(); } for (QueryPart p : parts) { builder.append(p.part); builder.append(params.getOrDefault(p.paramName, ClickHouseValues.NULL_EXPR)); } appendLastPartIfExists(builder); } /** * Applies stringified parameters to the given string builder. * * @param builder non-null string builder * @param params stringified parameters */ public void apply(StringBuilder builder, Collection params) { if (params == null || params.isEmpty()) { apply(builder, Collections.emptyMap()); return; } Map map = null; Iterator it = params.iterator(); if (it.hasNext()) { map = new HashMap<>(); for (String n : names.keySet()) { String v = it.next(); if (v != null) { map.put(n, v); } if (!it.hasNext()) { break; } } } apply(builder, map); } /** * Applies raw parameters to the given string builder. * {@link ClickHouseValues#convertToSqlExpression(Object)} will be used to * stringify the parameters. * * @param builder non-null string builder * @param param raw parameter * @param more more raw parameters if any */ public void apply(StringBuilder builder, Object param, Object... more) { if (!hasParameter()) { builder.append(originalQuery); return; } int len = more == null ? 0 : more.length; Map map = new HashMap<>(); int index = -1; for (Entry e : names.entrySet()) { ClickHouseValue v = e.getValue(); if (index < 0) { map.put(e.getKey(), v != null ? v.update(param).toSqlExpression() : ClickHouseValues.convertToSqlExpression(param)); } else if (index < len) { map.put(e.getKey(), v != null ? v.update(more[index]).toSqlExpression() // NOSONAR : ClickHouseValues.convertToSqlExpression(more[index])); // NOSONAR } else { break; } index++; } apply(builder, map); } /** * Applies raw parameters to the given string builder. * {@link ClickHouseValues#convertToSqlExpression(Object)} will be used to * stringify the parameters. * * @param builder non-null string builder * @param values raw parameters */ public void apply(StringBuilder builder, Object[] values) { int len = values == null ? 0 : values.length; if (len == 0) { apply(builder, Collections.emptyMap()); return; } Map map = new HashMap<>(); int index = 0; for (Entry e : names.entrySet()) { ClickHouseValue v = e.getValue(); if (index < len) { map.put(e.getKey(), v != null ? v.update(values[index]).toSqlExpression() : ClickHouseValues.convertToSqlExpression(values[index])); } else { break; } index++; } apply(builder, map); } /** * Applies stringified parameters to the given string builder. * * @param builder non-null string builder * @param param stringified parameter * @param more more stringified parameters if any */ public void apply(StringBuilder builder, String param, String... more) { if (!hasParameter()) { builder.append(originalQuery); return; } int len = more == null ? 0 : more.length; Map map = new HashMap<>(); int index = -1; for (String n : names.keySet()) { if (index < 0) { map.put(n, param); } else if (index < len) { map.put(n, more[index]); // NOSONAR } else { break; } index++; } apply(builder, map); } /** * Applies stringified parameters to the given string builder. * * @param builder non-null string builder * @param values stringified parameters */ public void apply(StringBuilder builder, String[] values) { int len = values == null ? 0 : values.length; if (len == 0) { apply(builder, Collections.emptyMap()); return; } Map map = new HashMap<>(); int index = 0; for (String n : names.keySet()) { if (index < len) { map.put(n, values[index]); } else { break; } index++; } apply(builder, map); } /** * Gets named parameters. * * @return list of named parameters */ public List getParameters() { if (names.isEmpty()) { return Collections.emptyList(); } List list = new ArrayList<>(names.size()); for (String n : names.keySet()) { list.add(n); } return Collections.unmodifiableList(list); } /** * Gets original query. * * @return original query */ public String getOriginalQuery() { return originalQuery; } /** * Gets query parts. Each part is composed of a snippet taken from * {@link #getOriginalQuery()}, followed by a parameter name, which might be * null. * * @return query parts */ public List getQueryParts() { List queryParts = new ArrayList<>(parts.size() + 1); for (QueryPart p : parts) { queryParts.add(new String[] { p.part, p.paramName }); } if (lastPart != null) { queryParts.add(new String[] { lastPart, null }); } return queryParts; } /** * Gets parameter templates for converting value to SQL expression. * * @return parameter templates */ public ClickHouseValue[] getParameterTemplates() { int i = 0; ClickHouseValue[] templates = new ClickHouseValue[names.size()]; for (ClickHouseValue v : names.values()) { templates[i++] = v; } return templates; } /** * Checks if the query has at least one parameter or not. * * @return true if there's at least one parameter; false otherwise */ public boolean hasParameter() { return !names.isEmpty(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((lastPart == null) ? 0 : lastPart.hashCode()); result = prime * result + names.hashCode(); result = prime * result + originalQuery.hashCode(); result = prime * result + parts.hashCode(); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } ClickHouseParameterizedQuery other = (ClickHouseParameterizedQuery) obj; return Objects.equals(lastPart, other.lastPart) && names.equals(other.names) && originalQuery.equals(other.originalQuery) && parts.equals(other.parts); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy