/* Copyright (c) 1995-2000, The Hypersonic SQL Group. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the Hypersonic SQL Group nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP, * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * This software consists of voluntary contributions made by many individuals * on behalf of the Hypersonic SQL Group. * * * For work added by the HSQL Development Group: * * Copyright (c) 2001-2008, The HSQL Development Group * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the HSQL Development Group nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG, * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.hsqldb; import org.hsqldb.index.RowIterator; import org.hsqldb.lib.ArrayUtil; import org.hsqldb.lib.HashMappedList; // fredt@users 20030813 - patch 1.7.2 - fix for column comparison within same table bugs #572075 and 722443 // fredt@users 20031012 - patch 1.7.2 - better OUTER JOIN implementation // fredt@users 20031026 - patch 1.7.2 - more efficient findfirst - especially for multi-column equijoins // implemented optimisations similart to patch 465542 by hjbush@users /** * This class iterates over table rows to select the rows that fulfil join * or other conditions. It uses indexes if they are availabe. * * Extended in successive versions of HSQLDB. * * @author Thomas Mueller (Hypersonic SQL Group) * @version 1.8.0 * @since Hypersonic SQL */ final class TableFilter { static final int CONDITION_NONE = -1; // not a condition expression static final int CONDITION_UNORDERED = 0; // not candidate for eStart or eEnd static final int CONDITION_START_END = 1; // candidate for eStart and eEnd static final int CONDITION_START = 2; // candidate for eStart static final int CONDITION_END = 3; // candidate for eEnd static final int CONDITION_OUTER = 4; // add to this Table filterTable; private String tableAlias; HashMappedList columnAliases; Index filterIndex; private Object[] emptyData; boolean[] usedColumns; private Expression eStart, eEnd; // Expression eAnd; // boolean isOuterJoin; // table joined with OUTER JOIN boolean isAssigned; // conditions have been assigned to this boolean isMultiFindFirst; // findFirst() uses multi-column index Expression[] findFirstExpressions; // expressions for column values // private RowIterator it; Object[] currentData; Row currentRow; // Object[] currentJoinData; // addendum to the result of findFirst() and next() with isOuterJoin==true // when the result is false, it indicates if a non-join condition caused the failure boolean nonJoinIsNull; // indicates current data is empty data produced for an outer join boolean isCurrentOuter; /** * Constructor declaration * * * @param t * @param alias * @param outerjoin */ TableFilter(Table t, String alias, HashMappedList columnList, boolean outerjoin) { filterTable = t; tableAlias = alias == null ? t.getName().name : alias; columnAliases = columnList; isOuterJoin = outerjoin; emptyData = filterTable.getEmptyRowData(); usedColumns = filterTable.getNewColumnCheckList(); } /** * Returns the alias or the table name. * Never returns null; * @return */ String getName() { return tableAlias; } /** * Retrieves this object's filter Table object. * * @return this object's filter Table object */ Table getTable() { return filterTable; } /** * Retrieves a CONDITION_XXX code indicating how a condition * expression can be used for a TableFilter. * * @param exprType an expression type code * @return */ static int getConditionType(Expression e) { int exprType = e.getType(); switch (exprType) { case Expression.NOT_EQUAL : case Expression.LIKE : return CONDITION_UNORDERED; case Expression.IN : { return e.isQueryCorrelated ? CONDITION_NONE : CONDITION_UNORDERED; } case Expression.IS_NULL : case Expression.EQUAL : { return CONDITION_START_END; } case Expression.BIGGER : case Expression.BIGGER_EQUAL : { return CONDITION_START; } case Expression.SMALLER : case Expression.SMALLER_EQUAL : { return CONDITION_END; } default : { // not a condition so forget it return CONDITION_NONE; } } } // TODO: Optimize // // The current way always chooses eStart, eEnd conditions // using first encountered eligible index // // We should check if current index offers better selectivity/access // path than previously assigned iIndex. // // EXAMPLE 1: // // CREATE TABLE t (c1 int, c2 int primary key) // CREATE INDEX I1 ON t(c1) // SELECT // * // FROM // t // WHERE // c1 = | < | <= | >= | > ... // AND // c2 = | < | <= | >= | > ... // // currently always chooses iIndex / condition (c1/I1), over // index / condition (c2/pk), whereas index / condition (c2/pk) // may well be better, especially if condition on c2 is equality // (condition_start_end) and conditionon(s) on c1 involve range // (condition_start, condition_end, or some composite). // // Currently, the developer/client software must somehow know facts // both about the table, the query and the way HSQLDB forms its // plans and, based on this knowlege, perhaps decide to reverse // order by explicitly issuing instead: // // SELECT // * // FROM // t // WHERE // c2 = | < | <= | >= | > ... // AND // c1 = | < | <= | >= | > ... // // to get optimal index choice. // // The same thing applies to and is even worse for joins. // // Consider the following (highly artificial, but easy to // understand) case: // // CREATE TABLE T1(ID INTEGER PRIMARY KEY, C1 INTEGER) // CREATE INDEX I1 ON T1(C1) // CREATE TABLE T2(ID INTEGER PRIMARY KEY, C1 INTEGER) // CREATE INDEX I2 ON T2(C1) // // select * from t1, t2 where t1.c1 = t2.c1 and t1.id = t2.id // // Consider the worst value distribution where t1 and t2 are both // 10,000 rows, c1 selectivity is nil (all values are identical) // for both tables, and, say, id values span the range 0..9999 // for both tables. // // Then time to completion on 500 MHz Athlon testbed using memory // tables is: // // 10000 row(s) in 309114 ms // // whereas for: // // select * from t1, t2 where t1.id = t2.id and t1.c1 = t2.c1 // // time to completion is: // // 10000 row(s) in 471 ms // // Hence, the unoptimized query takes 656 times as long as the // optimized one!!! // // EXAMPLE 2: // // If there are, say, two non-unique candidate indexes, // and some range or equality predicates against // them, preference should be given to the one with // better selectivity (if the total row count of the // table is large, otherwise the overhead of making // the choice is probably large w.r.t. any possible // savings). Might require maintaining some basic // statistics or performing appropriate index probes // at the time the plan is being generated. /** * Chooses certain query conditions and assigns a copy of them to this * filter. The original condition is set to Expression.TRUE once assigned. * * @param condition * * @throws HsqlException */ void setConditions(Session session, Expression condition) throws HsqlException { setCondition(session, condition); if (filterIndex == null) { filterIndex = filterTable.getPrimaryIndex(); } if (filterIndex.getVisibleColumns() == 1 || eStart == null || eAnd == null || eStart.exprType != Expression.EQUAL) { return; } boolean[] check = filterTable.getNewColumnCheckList(); Expression[] expr = new Expression[check.length]; int colindex = eStart.getArg().getColumnNr(); check[colindex] = true; expr[colindex] = eStart.getArg2(); eAnd.getEquiJoinColumns(this, check, expr); if (ArrayUtil.containsAllTrueElements(check, filterIndex.colCheck)) { isMultiFindFirst = true; findFirstExpressions = expr; } } private void setCondition(Session session, Expression e) throws HsqlException { int type = e.getType(); Expression e1 = e.getArg(); Expression e2 = e.getArg2(); isAssigned = true; if (type == Expression.AND) { setCondition(session, e1); setCondition(session, e2); return; } if (type == Expression.OR && isOuterJoin && e.isInJoin && e.outerFilter == this) { addAndCondition(e); e.setTrue(); return; } int conditionType = getConditionType(e); if (conditionType == CONDITION_NONE) { // not a condition expression return; } // fredt@users 20030813 - patch 1.7.2 - fix for column comparison within same table bugs #572075 and 722443 if (e1.getFilter() == this && e2.getFilter() == this) { conditionType = CONDITION_UNORDERED; } else if (e1.getFilter() == this) { if (!e.isInJoin && isOuterJoin) { // do not use a where condition on the second table in outer joins return; } // ok include this } else if ((e2.getFilter() == this) && (conditionType != CONDITION_UNORDERED)) { // swap and try again to allow index usage e.swapCondition(); setCondition(session, e); return; } else if (e1.outerFilter == this) { // fredt - this test is last to allow swapping the terms above conditionType = CONDITION_OUTER; } else { // unrelated: don't include return; } // Trace.doAssert(e1.getFilter() == this, "setCondition"); if (!e2.isResolved()) { return; } // fredt - condition defined in outer but not this one if (e1.outerFilter != null && e1.outerFilter != this) { return; } if (conditionType == CONDITION_UNORDERED) { addAndCondition(e); return; } if (conditionType == CONDITION_OUTER) { addAndCondition(e); return; } int i = e1.getColumnNr(); Index index = filterTable.getIndexForColumn(session, i); if (index == null || (filterIndex != index && filterIndex != null)) { addAndCondition(e); return; } filterIndex = index; switch (conditionType) { case CONDITION_START_END : { // candidate for both start and end if ((eStart != null) || (eEnd != null)) { addAndCondition(e); return; } eStart = new Expression(e); eEnd = eStart; break; } case CONDITION_START : { // candidate for start if (eStart != null) { addAndCondition(e); return; } eStart = new Expression(e); break; } case CONDITION_END : { // candidate for end if (eEnd != null) { addAndCondition(e); return; } eEnd = new Expression(e); break; } } e.setTrue(); } /** * Finds the first row in the table (using an index if there is one) and * checks it against the eEnd (range) and eAnd (other conditions) * Expression objects. (fredt) * * @return true if first row was found, else false */ boolean findFirst(Session session) throws HsqlException { nonJoinIsNull = false; isCurrentOuter = false; if (filterIndex == null) { filterIndex = filterTable.getPrimaryIndex(); } if (isMultiFindFirst) { boolean convertible = true; int[] types = filterTable.getColumnTypes(); currentJoinData = filterTable.getEmptyRowData(); for (int i = 0; i < findFirstExpressions.length; i++) { Expression e = findFirstExpressions[i]; if (e != null) { Object value = e.getValue(session); if (Column.compareToTypeRange(value, types[i]) != 0) { convertible = false; break; } value = Column.convertObject(value, types[i]); currentJoinData[i] = e.getValue(session, types[i]); } } it = convertible ? filterIndex.findFirstRow(session, currentJoinData) : filterIndex.emptyIterator(); if (!it.hasNext()) { ArrayUtil.clearArray(ArrayUtil.CLASS_CODE_OBJECT, currentJoinData, 0, currentJoinData.length); } } else if (eStart == null) { it = eEnd == null ? filterIndex.firstRow(session) : filterIndex.findFirstRowNotNull(session); } else { Object value = eStart.getArg2().getValue(session); int valuetype = eStart.getArg2().getDataType(); int targettype = eStart.getArg().getDataType(); it = getFirstIterator(session, eStart.getType(), value, valuetype, filterIndex, targettype); } while (true) { currentRow = it.next(); if (currentRow == null) { break; } currentData = currentRow.getData(); if (!(eEnd == null || eEnd.testCondition(session))) { break; } if (eAnd == null || eAnd.testCondition(session)) { return true; } } currentRow = null; currentData = emptyData; return false; } static RowIterator getFirstIterator(Session session, int eType, Object value, int valueType, Index index, int targetType) throws HsqlException { RowIterator it; int range = 0; if (targetType != valueType) { range = Column.compareToTypeRange(value, targetType); } if (range == 0) { value = Column.convertObject(value, targetType); it = index.findFirstRow(session, value, eType); } else { switch (eType) { case Expression.BIGGER_EQUAL : case Expression.BIGGER : if (range < 0) { it = index.findFirstRowNotNull(session); break; } default : it = index.emptyIterator(); } } return it; } /** * Advances to the next available value. <p> * * @return true if a next value is available upon exit * * @throws HsqlException if a database access error occurs */ boolean next(Session session) throws HsqlException { boolean result = false; nonJoinIsNull = false; isCurrentOuter = false; while (true) { currentRow = it.next(); if (currentRow == null) { break; } currentData = currentRow.getData(); if (!(eEnd == null || eEnd.testCondition(session))) { break; } if (eAnd == null || eAnd.testCondition(session)) { result = true; break; } } if (result) { return true; } currentRow = null; currentData = emptyData; return false; } boolean nextOuter(Session session) throws HsqlException { nonJoinIsNull = false; isCurrentOuter = true; currentData = emptyData; currentRow = null; return eAnd == null || (eAnd.getFilter() != this && eAnd.isInJoin) || eAnd.testCondition(session); } /** * Forms a new conjunction using the given condition and this filter's * pre-existing AND condition, or sets the given condition as this filter's * AND condition when there is no such pre-exisiting object. * * @param e the condition to add */ private void addAndCondition(Expression e) { Expression e2 = new Expression(e); if (eAnd == null) { eAnd = e2; } else { Expression and = new Expression(Expression.AND, eAnd, e2); eAnd = and; } e.setTrue(); } /** * Removes reference to Index to avoid possible memory leaks after alter * table or drop index */ void setAsCheckFilter() { filterIndex = null; } // boucheb@users 20030415 - added for debugging support /** * Retreives a String representation of this obejct. <p> * * The returned String describes this object's table, alias * access mode, index, join mode, Start, End and And conditions. * * @return a String representation of this object */ public String describe(Session session) { StringBuffer sb; String temp; Index index; Index primaryIndex; int[] primaryKey; boolean hidden; boolean fullScan; sb = new StringBuffer(); index = filterIndex; primaryIndex = filterTable.getPrimaryIndex(); primaryKey = filterTable.getPrimaryKey(); hidden = false; fullScan = (eStart == null && eEnd == null); if (index == null) { index = primaryIndex; } if (index == primaryIndex && primaryKey.length == 0) { hidden = true; fullScan = true; } sb.append(super.toString()).append('\n'); sb.append("table=[").append(filterTable.getName().name).append("]\n"); sb.append("alias=[").append(tableAlias).append("]\n"); sb.append("access=[").append(fullScan ? "FULL SCAN" : "INDEX PRED").append("]\n"); sb.append("index=["); sb.append(index == null ? "NONE" : index.getName() == null ? "UNNAMED" : index.getName() .name); sb.append(hidden ? "[HIDDEN]]\n" : "]\n"); sb.append("isOuterJoin=[").append(isOuterJoin).append("]\n"); temp = eStart == null ? "null" : eStart.describe(session); sb.append("eStart=[").append(temp).append("]\n"); temp = eEnd == null ? "null" : eEnd.describe(session); sb.append("eEnd=[").append(temp).append("]\n"); temp = eAnd == null ? "null" : eAnd.describe(session); sb.append("eAnd=[").append(temp).append("]"); return sb.toString(); } }