/* * 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.ignite.internal.processors.odbc.odbc; import org.apache.ignite.IgniteCache; import org.apache.ignite.IgniteLogger; import org.apache.ignite.cache.query.QueryCursor; import org.apache.ignite.cache.query.SqlFieldsQuery; import org.apache.ignite.internal.GridKernalContext; import org.apache.ignite.internal.binary.GridBinaryMarshaller; import org.apache.ignite.internal.processors.cache.QueryCursorImpl; import org.apache.ignite.internal.processors.odbc.OdbcQueryGetColumnsMetaRequest; import org.apache.ignite.internal.processors.odbc.OdbcQueryGetColumnsMetaResult; import org.apache.ignite.internal.processors.odbc.OdbcQueryGetParamsMetaRequest; import org.apache.ignite.internal.processors.odbc.OdbcQueryGetParamsMetaResult; import org.apache.ignite.internal.processors.odbc.OdbcQueryGetTablesMetaRequest; import org.apache.ignite.internal.processors.odbc.OdbcQueryGetTablesMetaResult; import org.apache.ignite.internal.processors.odbc.OdbcTableMeta; import org.apache.ignite.internal.processors.odbc.OdbcUtils; import org.apache.ignite.internal.processors.odbc.SqlListenerColumnMeta; import org.apache.ignite.internal.processors.odbc.SqlListenerQueryCloseRequest; import org.apache.ignite.internal.processors.odbc.SqlListenerQueryCloseResult; import org.apache.ignite.internal.processors.odbc.SqlListenerQueryExecuteRequest; import org.apache.ignite.internal.processors.odbc.SqlListenerQueryExecuteResult; import org.apache.ignite.internal.processors.odbc.SqlListenerQueryFetchRequest; import org.apache.ignite.internal.processors.odbc.SqlListenerQueryFetchResult; import org.apache.ignite.internal.processors.odbc.SqlListenerRequest; import org.apache.ignite.internal.processors.odbc.SqlListenerRequestHandler; import org.apache.ignite.internal.processors.odbc.SqlListenerResponse; import org.apache.ignite.internal.processors.odbc.odbc.escape.OdbcEscapeUtils; import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata; import org.apache.ignite.internal.processors.query.GridQueryTypeDescriptor; import org.apache.ignite.internal.util.GridSpinBusyLock; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.lang.IgniteBiTuple; import java.sql.ParameterMetaData; import java.sql.PreparedStatement; import java.sql.Types; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import static org.apache.ignite.internal.processors.odbc.SqlListenerRequest.META_COLS; import static org.apache.ignite.internal.processors.odbc.SqlListenerRequest.META_PARAMS; import static org.apache.ignite.internal.processors.odbc.SqlListenerRequest.META_TBLS; import static org.apache.ignite.internal.processors.odbc.SqlListenerRequest.QRY_CLOSE; import static org.apache.ignite.internal.processors.odbc.SqlListenerRequest.QRY_EXEC; import static org.apache.ignite.internal.processors.odbc.SqlListenerRequest.QRY_FETCH; /** * SQL query handler. */ public class OdbcRequestHandler implements SqlListenerRequestHandler { /** Query ID sequence. */ private static final AtomicLong QRY_ID_GEN = new AtomicLong(); /** Kernel context. */ private final GridKernalContext ctx; /** Logger. */ private final IgniteLogger log; /** Busy lock. */ private final GridSpinBusyLock busyLock; /** Maximum allowed cursors. */ private final int maxCursors; /** Current queries cursors. */ private final ConcurrentHashMap<Long, IgniteBiTuple<QueryCursor, Iterator>> qryCursors = new ConcurrentHashMap<>(); /** Distributed joins flag. */ private final boolean distributedJoins; /** Enforce join order flag. */ private final boolean enforceJoinOrder; /** * Constructor. * * @param ctx Context. * @param busyLock Shutdown latch. * @param maxCursors Maximum allowed cursors. * @param distributedJoins Distributed joins flag. * @param enforceJoinOrder Enforce join order flag. */ public OdbcRequestHandler(GridKernalContext ctx, GridSpinBusyLock busyLock, int maxCursors, boolean distributedJoins, boolean enforceJoinOrder) { this.ctx = ctx; this.busyLock = busyLock; this.maxCursors = maxCursors; this.distributedJoins = distributedJoins; this.enforceJoinOrder = enforceJoinOrder; log = ctx.log(getClass()); } /** {@inheritDoc} */ @Override public SqlListenerResponse handle(SqlListenerRequest req) { assert req != null; if (!busyLock.enterBusy()) return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, "Failed to handle ODBC request because node is stopping: " + req); try { switch (req.command()) { case QRY_EXEC: return executeQuery((SqlListenerQueryExecuteRequest)req); case QRY_FETCH: return fetchQuery((SqlListenerQueryFetchRequest)req); case QRY_CLOSE: return closeQuery((SqlListenerQueryCloseRequest)req); case META_COLS: return getColumnsMeta((OdbcQueryGetColumnsMetaRequest)req); case META_TBLS: return getTablesMeta((OdbcQueryGetTablesMetaRequest)req); case META_PARAMS: return getParamsMeta((OdbcQueryGetParamsMetaRequest)req); } return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, "Unsupported ODBC request: " + req); } finally { busyLock.leaveBusy(); } } /** * {@link SqlListenerQueryExecuteRequest} command handler. * * @param req Execute query request. * @return Response. */ private SqlListenerResponse executeQuery(SqlListenerQueryExecuteRequest req) { int cursorCnt = qryCursors.size(); if (maxCursors > 0 && cursorCnt >= maxCursors) return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, "Too many opened cursors (either close other " + "opened cursors or increase the limit through OdbcConfiguration.setMaxOpenCursors()) " + "[maximum=" + maxCursors + ", current=" + cursorCnt + ']'); long qryId = QRY_ID_GEN.getAndIncrement(); try { String sql = OdbcEscapeUtils.parse(req.sqlQuery()); if (log.isDebugEnabled()) log.debug("ODBC query parsed [reqId=" + req.requestId() + ", original=" + req.sqlQuery() + ", parsed=" + sql + ']'); SqlFieldsQuery qry = new SqlFieldsQuery(sql); qry.setArgs(req.arguments()); qry.setDistributedJoins(distributedJoins); qry.setEnforceJoinOrder(enforceJoinOrder); IgniteCache<Object, Object> cache0 = ctx.grid().cache(req.cacheName()); if (cache0 == null) return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, "Cache doesn't exist (did you configure it?): " + req.cacheName()); IgniteCache<Object, Object> cache = cache0.withKeepBinary(); if (cache == null) return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, "Can not get cache with keep binary: " + req.cacheName()); QueryCursor qryCur = cache.query(qry); qryCursors.put(qryId, new IgniteBiTuple<QueryCursor, Iterator>(qryCur, null)); List<?> fieldsMeta = ((QueryCursorImpl) qryCur).fieldsMeta(); SqlListenerQueryExecuteResult res = new SqlListenerQueryExecuteResult(qryId, convertMetadata(fieldsMeta)); return new SqlListenerResponse(res); } catch (Exception e) { qryCursors.remove(qryId); U.error(log, "Failed to execute SQL query [reqId=" + req.requestId() + ", req=" + req + ']', e); return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, e.toString()); } } /** * {@link SqlListenerQueryCloseRequest} command handler. * * @param req Execute query request. * @return Response. */ private SqlListenerResponse closeQuery(SqlListenerQueryCloseRequest req) { try { IgniteBiTuple<QueryCursor, Iterator> tuple = qryCursors.get(req.queryId()); if (tuple == null) return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, "Failed to find query with ID: " + req.queryId()); QueryCursor cur = tuple.get1(); assert(cur != null); cur.close(); qryCursors.remove(req.queryId()); SqlListenerQueryCloseResult res = new SqlListenerQueryCloseResult(req.queryId()); return new SqlListenerResponse(res); } catch (Exception e) { qryCursors.remove(req.queryId()); U.error(log, "Failed to close SQL query [reqId=" + req.requestId() + ", req=" + req.queryId() + ']', e); return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, e.toString()); } } /** * {@link SqlListenerQueryFetchRequest} command handler. * * @param req Execute query request. * @return Response. */ private SqlListenerResponse fetchQuery(SqlListenerQueryFetchRequest req) { try { IgniteBiTuple<QueryCursor, Iterator> tuple = qryCursors.get(req.queryId()); if (tuple == null) return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, "Failed to find query with ID: " + req.queryId()); Iterator iter = tuple.get2(); if (iter == null) { QueryCursor cur = tuple.get1(); iter = cur.iterator(); tuple.put(cur, iter); } List<Object> items = new ArrayList<>(); for (int i = 0; i < req.pageSize() && iter.hasNext(); ++i) items.add(iter.next()); SqlListenerQueryFetchResult res = new SqlListenerQueryFetchResult(req.queryId(), items, !iter.hasNext()); return new SqlListenerResponse(res); } catch (Exception e) { U.error(log, "Failed to fetch SQL query result [reqId=" + req.requestId() + ", req=" + req + ']', e); return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, e.toString()); } } /** * {@link OdbcQueryGetColumnsMetaRequest} command handler. * * @param req Get columns metadata request. * @return Response. */ private SqlListenerResponse getColumnsMeta(OdbcQueryGetColumnsMetaRequest req) { try { List<SqlListenerColumnMeta> meta = new ArrayList<>(); String cacheName; String tableName; if (req.tableName().contains(".")) { // Parsing two-part table name. String[] parts = req.tableName().split("\\."); cacheName = OdbcUtils.removeQuotationMarksIfNeeded(parts[0]); tableName = parts[1]; } else { cacheName = OdbcUtils.removeQuotationMarksIfNeeded(req.cacheName()); tableName = req.tableName(); } Collection<GridQueryTypeDescriptor> tablesMeta = ctx.query().types(cacheName); for (GridQueryTypeDescriptor table : tablesMeta) { if (!matches(table.name(), tableName)) continue; for (Map.Entry<String, Class<?>> field : table.fields().entrySet()) { if (!matches(field.getKey(), req.columnName())) continue; SqlListenerColumnMeta columnMeta = new SqlListenerColumnMeta(req.cacheName(), table.name(), field.getKey(), field.getValue()); if (!meta.contains(columnMeta)) meta.add(columnMeta); } } OdbcQueryGetColumnsMetaResult res = new OdbcQueryGetColumnsMetaResult(meta); return new SqlListenerResponse(res); } catch (Exception e) { U.error(log, "Failed to get columns metadata [reqId=" + req.requestId() + ", req=" + req + ']', e); return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, e.toString()); } } /** * {@link OdbcQueryGetTablesMetaRequest} command handler. * * @param req Get tables metadata request. * @return Response. */ private SqlListenerResponse getTablesMeta(OdbcQueryGetTablesMetaRequest req) { try { List<OdbcTableMeta> meta = new ArrayList<>(); String realSchema = OdbcUtils.removeQuotationMarksIfNeeded(req.schema()); for (String cacheName : ctx.cache().cacheNames()) { if (!matches(cacheName, realSchema)) continue; Collection<GridQueryTypeDescriptor> tablesMeta = ctx.query().types(cacheName); for (GridQueryTypeDescriptor table : tablesMeta) { if (!matches(table.name(), req.table())) continue; if (!matches("TABLE", req.tableType())) continue; OdbcTableMeta tableMeta = new OdbcTableMeta(null, cacheName, table.name(), "TABLE"); if (!meta.contains(tableMeta)) meta.add(tableMeta); } } OdbcQueryGetTablesMetaResult res = new OdbcQueryGetTablesMetaResult(meta); return new SqlListenerResponse(res); } catch (Exception e) { U.error(log, "Failed to get tables metadata [reqId=" + req.requestId() + ", req=" + req + ']', e); return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, e.toString()); } } /** * {@link OdbcQueryGetParamsMetaRequest} command handler. * * @param req Get params metadata request. * @return Response. */ private SqlListenerResponse getParamsMeta(OdbcQueryGetParamsMetaRequest req) { try { PreparedStatement stmt = ctx.query().prepareNativeStatement(req.cacheName(), req.query()); ParameterMetaData pmd = stmt.getParameterMetaData(); byte[] typeIds = new byte[pmd.getParameterCount()]; for (int i = 1; i <= pmd.getParameterCount(); ++i) { int sqlType = pmd.getParameterType(i); typeIds[i - 1] = sqlTypeToBinary(sqlType); } OdbcQueryGetParamsMetaResult res = new OdbcQueryGetParamsMetaResult(typeIds); return new SqlListenerResponse(res); } catch (Exception e) { U.error(log, "Failed to get params metadata [reqId=" + req.requestId() + ", req=" + req + ']', e); return new SqlListenerResponse(SqlListenerResponse.STATUS_FAILED, e.toString()); } } /** * Convert {@link java.sql.Types} to binary type constant (See {@link GridBinaryMarshaller} constants). * * @param sqlType SQL type. * @return Binary type. */ private static byte sqlTypeToBinary(int sqlType) { switch (sqlType) { case Types.BIGINT: return GridBinaryMarshaller.LONG; case Types.BOOLEAN: return GridBinaryMarshaller.BOOLEAN; case Types.DATE: return GridBinaryMarshaller.DATE; case Types.DOUBLE: return GridBinaryMarshaller.DOUBLE; case Types.FLOAT: case Types.REAL: return GridBinaryMarshaller.FLOAT; case Types.NUMERIC: case Types.DECIMAL: return GridBinaryMarshaller.DECIMAL; case Types.INTEGER: return GridBinaryMarshaller.INT; case Types.SMALLINT: return GridBinaryMarshaller.SHORT; case Types.TIME: return GridBinaryMarshaller.TIME; case Types.TIMESTAMP: return GridBinaryMarshaller.TIMESTAMP; case Types.TINYINT: return GridBinaryMarshaller.BYTE; case Types.CHAR: case Types.VARCHAR: case Types.LONGNVARCHAR: return GridBinaryMarshaller.STRING; case Types.NULL: return GridBinaryMarshaller.NULL; case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: default: return GridBinaryMarshaller.BYTE_ARR; } } /** * Convert metadata in collection from {@link GridQueryFieldMetadata} to * {@link SqlListenerColumnMeta}. * * @param meta Internal query field metadata. * @return Odbc query field metadata. */ private static Collection<SqlListenerColumnMeta> convertMetadata(Collection<?> meta) { List<SqlListenerColumnMeta> res = new ArrayList<>(); if (meta != null) { for (Object info : meta) { assert info instanceof GridQueryFieldMetadata; res.add(new SqlListenerColumnMeta((GridQueryFieldMetadata)info)); } } return res; } /** * Checks whether string matches SQL pattern. * * @param str String. * @param ptrn Pattern. * @return Whether string matches pattern. */ private static boolean matches(String str, String ptrn) { return str != null && (F.isEmpty(ptrn) || str.toUpperCase().matches(ptrn.toUpperCase().replace("%", ".*").replace("_", "."))); } }