diff --git a/h2/src/main/org/h2/command/Parser.java b/h2/src/main/org/h2/command/Parser.java index 1a11089a3d..19e23ec2e2 100644 --- a/h2/src/main/org/h2/command/Parser.java +++ b/h2/src/main/org/h2/command/Parser.java @@ -39,6 +39,9 @@ import static org.h2.command.Token.SMALLER_EQUAL; import static org.h2.command.Token.SPATIAL_INTERSECTS; import static org.h2.command.Token.TILDE; +import static org.h2.util.HasSQL.DEFAULT_SQL_FLAGS; +import static org.h2.util.HasSQL.QUOTE_ONLY_WHEN_REQUIRED; +import static org.h2.util.HasSQL.TRACE_SQL_FLAGS; import static org.h2.util.ParserUtil.ALL; import static org.h2.util.ParserUtil.AND; import static org.h2.util.ParserUtil.ANY; @@ -362,6 +365,7 @@ import org.h2.schema.Sequence; import org.h2.schema.UserAggregate; import org.h2.schema.UserDefinedFunction; +import org.h2.table.CTE; import org.h2.table.Column; import org.h2.table.DataChangeDeltaTable; import org.h2.table.DataChangeDeltaTable.ResultOption; @@ -376,7 +380,6 @@ import org.h2.table.Table; import org.h2.table.TableFilter; import org.h2.table.TableView; -import org.h2.util.HasSQL; import org.h2.util.IntervalUtils; import org.h2.util.ParserUtil; import org.h2.util.StringUtils; @@ -428,6 +431,7 @@ public final class Parser extends ParserBase { private int orderInFrom; private boolean parseDomainConstraint; private QueryScope queryScope; + private boolean parsingRecursiveWithList; /** * Creates a new instance of parser. @@ -1200,7 +1204,7 @@ private Prepared parseShow() { if (i > 0) { searchPathBuff.append(", "); } - ParserUtil.quoteIdentifier(searchPathBuff, searchPath[i], HasSQL.QUOTE_ONLY_WHEN_REQUIRED); + ParserUtil.quoteIdentifier(searchPathBuff, searchPath[i], QUOTE_ONLY_WHEN_REQUIRED); } } StringUtils.quoteStringSQL(buff, searchPathBuff.toString()); @@ -1813,7 +1817,7 @@ private TableFilter readDerivedTableWithCorrelation() { if (derivedColumnNames != null) { query.init(); columnTemplates = QueryExpressionTable.createQueryColumnTemplateList( - derivedColumnNames.toArray(new String[0]), query, new String[1]) + derivedColumnNames.toArray(new String[0]), query) .toArray(new Column[0]); } table = query.toTable(alias, columnTemplates, queryParameters, createView != null, currentSelect); @@ -1834,7 +1838,7 @@ private TableFilter buildTableFilter(Table table, String alias, ArrayList columns = Utils.newSmallArrayList(); String[] cols = null; - - // column names are now optional - they can be inferred from the named - // query, if not supplied by user - if (readIf(OPEN_PAREN)) { + readColumns: { + if (isPotentiallyRecursive) { + read(OPEN_PAREN); + } else if (!readIf(OPEN_PAREN)) { + break readColumns; + } cols = parseColumnList(); for (String c : cols) { // we don't really know the type of the column, so STRING will @@ -6931,68 +6940,58 @@ private void parseSingleCommonTableExpression(boolean isTemporary) { columns.add(new Column(c, TypeInfo.TYPE_VARCHAR)); } } - Table oldViewFound = getWithSubquery(cteViewName); + Table oldViewFound = getWithSubquery(cteName); if (oldViewFound != null) { - throw DbException.get(ErrorCode.TABLE_OR_VIEW_ALREADY_EXISTS_1, cteViewName); + throw DbException.get(ErrorCode.TABLE_OR_VIEW_ALREADY_EXISTS_1, cteName); } - /* - * This table is created as a workaround because recursive table - * expressions need to reference something that look like themselves to - * work (its removed after creation in this method). Only create table - * data and table if we don't have a working CTE already. - */ - Table recursiveTable = new ShadowTable(schema, cteViewName, columns.toArray(new Column[0])); - List columnTemplateList; - String[] querySQLOutput = new String[1]; - BitSet outerUsedParameters = openParametersScope(); - queryScope.tableSubqeries.put(cteViewName, recursiveTable); + read(AS); + read(OPEN_PAREN); + int index = tokenIndex; + setTokenIndex(index); + Query withQuery; + String sql; ArrayList queryParameters; - try { - read(AS); - read(OPEN_PAREN); - Query withQuery = parseQuery(); - if (!isTemporary) { - withQuery.session = session; + List columnTemplateList; + if (isPotentiallyRecursive) { + /* + * This table is created as a workaround because recursive table + * expressions need to reference something that look like themselves to + * work (its removed after creation in this method). Only create table + * data and table if we don't have a working CTE already. + */ + Table recursiveTable = new ShadowTable(database.getMainSchema(), cteName, columns.toArray(new Column[0])); + BitSet outerUsedParameters = openParametersScope(); + queryScope.tableSubqeries.put(cteName, recursiveTable); + try { + withQuery = parseQuery(); + } finally { + queryParameters = closeParametersScope(outerUsedParameters); + queryScope.tableSubqeries.remove(cteName); } - read(CLOSE_PAREN); - columnTemplateList = QueryExpressionTable.createQueryColumnTemplateList(cols, withQuery, querySQLOutput); - - } finally { - queryParameters = closeParametersScope(outerUsedParameters); - queryScope.tableSubqeries.remove(cteViewName); - } - - createCTEView(cteViewName, querySQLOutput[0], queryParameters, columnTemplateList, isTemporary); - } - - private void createCTEView(String cteViewName, String querySQL, ArrayList queryParameters, - List columnTemplateList, boolean isTemporary) { - Schema schema = getSchemaWithDefault(); - Column[] columnTemplateArray = columnTemplateList.toArray(new Column[0]); - - // No easy way to determine if this is a recursive query up front, so we just compile - // it twice - once without the flag set, and if we didn't see a recursive term, - // then we just compile it again. - TableView view; - session.lock(); - try { - view = new TableView(schema, 0, cteViewName, querySQL, - queryParameters, columnTemplateArray, session, - true, false, true, - isTemporary, queryScope); - if (!view.isRecursiveQueryDetected()) { - view = new TableView(schema, 0, cteViewName, querySQL, queryParameters, - columnTemplateArray, session, - false/* assume recursive */, false, true, - isTemporary, queryScope); + columnTemplateList = QueryExpressionTable.createQueryColumnTemplateList(cols, withQuery); + sql = withQuery.getPlanSQL(DEFAULT_SQL_FLAGS); + try { + withQuery = (Query) session.prepare(sql, false, true, queryScope); + columnTemplateList = QueryExpressionTable.createQueryColumnTemplateList(cols, withQuery); + sql = withQuery.getPlanSQL(DEFAULT_SQL_FLAGS); + isPotentiallyRecursive = false; + } catch (DbException e) { + // Assume a recursive query } - } finally { - session.unlock(); + } else { + BitSet outerUsedParameters = openParametersScope(); + try { + withQuery = parseQuery(); + } finally { + queryParameters = closeParametersScope(outerUsedParameters); + } + columnTemplateList = QueryExpressionTable.createQueryColumnTemplateList(cols, withQuery); + sql = withQuery.getPlanSQL(DEFAULT_SQL_FLAGS); } - view.setTableExpression(true); - view.setTemporary(isTemporary); - view.setOnCommitDrop(false); - queryScope.tableSubqeries.put(cteViewName, view); + read(CLOSE_PAREN); + queryScope.tableSubqeries.put(cteName, new CTE(cteName, withQuery, StringUtils.cache(sql), + queryParameters, columnTemplateList.toArray(new Column[0]), session, isPotentiallyRecursive, + queryScope)); } private CreateView parseCreateView(boolean force, boolean orReplace) { @@ -7279,7 +7278,7 @@ private boolean parseSequenceOptions(SequenceOptions options, CreateSequence com TypeInfo dataType = parseDataType(); if (!DataType.isNumericType(dataType.getValueType())) { throw DbException.getUnsupportedException(dataType - .getSQL(new StringBuilder("CREATE SEQUENCE AS "), HasSQL.TRACE_SQL_FLAGS).toString()); + .getSQL(new StringBuilder("CREATE SEQUENCE AS "), TRACE_SQL_FLAGS).toString()); } options.setDataType(dataType); } else if (readIf("START", WITH) diff --git a/h2/src/main/org/h2/command/ddl/CreateView.java b/h2/src/main/org/h2/command/ddl/CreateView.java index 62fa2e8438..b5e771db31 100644 --- a/h2/src/main/org/h2/command/ddl/CreateView.java +++ b/h2/src/main/org/h2/command/ddl/CreateView.java @@ -111,10 +111,9 @@ long update(Schema schema) { } } if (view == null) { - view = new TableView(schema, id, viewName, querySQL, null, columnTemplatesAsUnknowns, session, - false, false, false, false, null); + view = new TableView(schema, id, viewName, querySQL, columnTemplatesAsUnknowns, session); } else { - view.replace(querySQL, columnTemplatesAsUnknowns, session, false, force, false); + view.replace(querySQL, columnTemplatesAsUnknowns, session, force); view.setModified(); } if (comment != null) { diff --git a/h2/src/main/org/h2/command/query/Query.java b/h2/src/main/org/h2/command/query/Query.java index 481864c1d9..2094d58a92 100644 --- a/h2/src/main/org/h2/command/query/Query.java +++ b/h2/src/main/org/h2/command/query/Query.java @@ -31,13 +31,12 @@ import org.h2.result.ResultInterface; import org.h2.result.ResultTarget; import org.h2.result.SortOrder; +import org.h2.table.CTE; import org.h2.table.Column; import org.h2.table.ColumnResolver; import org.h2.table.DerivedTable; import org.h2.table.Table; import org.h2.table.TableFilter; -import org.h2.table.TableView; -import org.h2.util.ParserUtil; import org.h2.util.StringUtils; import org.h2.util.Utils; import org.h2.value.ExtTypeInfoRow; @@ -905,7 +904,7 @@ protected void writeWithList(StringBuilder builder, int sqlFlags) { if (withClause != null) { boolean recursive = false; for (Table t : withClause.values()) { - if (t instanceof TableView && ((TableView) t).isRecursive()) { + if (((CTE) t).isRecursive()) { recursive = true; break; } @@ -921,25 +920,14 @@ protected void writeWithList(StringBuilder builder, int sqlFlags) { } else { builder.append(",\n"); } - writeWithListElement(builder, sqlFlags, table); + table.getSQL(builder, sqlFlags).append('('); + Column.writeColumns(builder, table.getColumns(), sqlFlags).append(") AS (\n"); + StringUtils.indent(builder, ((CTE) table).getQuerySQL(), 4, true).append(')'); } builder.append('\n'); } } - protected static void writeWithListElement(StringBuilder builder, int sqlFlags, Table table) { - ParserUtil.quoteIdentifier(builder, table.getName(), sqlFlags).append('('); - Column.writeColumns(builder, table.getColumns(), sqlFlags).append(") AS "); - if (table instanceof TableView) { - String querySQL = ((TableView) table).getQuerySQL(); - if (querySQL != null) { - StringUtils.indent(builder.append("(\n"), querySQL, 4, true).append(')'); - return; - } - } - table.getSQL(builder, sqlFlags); - } - /** * Appends ORDER BY, OFFSET, and FETCH clauses to the plan. * diff --git a/h2/src/main/org/h2/index/QueryExpressionCursor.java b/h2/src/main/org/h2/index/QueryExpressionCursor.java index 77bd3e0575..9319ab83ba 100644 --- a/h2/src/main/org/h2/index/QueryExpressionCursor.java +++ b/h2/src/main/org/h2/index/QueryExpressionCursor.java @@ -47,7 +47,7 @@ public boolean next() { while (true) { boolean res = result.next(); if (!res) { - if (index.isRecursive()) { + if (index.getClass() == RecursiveIndex.class) { result.reset(); } else { result.close(); diff --git a/h2/src/main/org/h2/index/QueryExpressionIndex.java b/h2/src/main/org/h2/index/QueryExpressionIndex.java index 2130fda681..60b14345cc 100644 --- a/h2/src/main/org/h2/index/QueryExpressionIndex.java +++ b/h2/src/main/org/h2/index/QueryExpressionIndex.java @@ -6,125 +6,44 @@ package org.h2.index; import java.util.ArrayList; -import java.util.concurrent.TimeUnit; -import org.h2.api.ErrorCode; -import org.h2.command.Parser; -import org.h2.command.query.AllColumnsForPlan; import org.h2.command.query.Query; -import org.h2.command.query.SelectUnion; -import org.h2.engine.Constants; import org.h2.engine.SessionLocal; import org.h2.expression.Parameter; -import org.h2.expression.condition.Comparison; import org.h2.message.DbException; -import org.h2.result.LocalResult; -import org.h2.result.ResultInterface; import org.h2.result.Row; -import org.h2.result.SearchRow; -import org.h2.result.SortOrder; import org.h2.table.Column; -import org.h2.table.IndexColumn; import org.h2.table.QueryExpressionTable; -import org.h2.table.TableFilter; -import org.h2.table.TableView; -import org.h2.util.IntArray; -import org.h2.value.Value; /** * This object represents a virtual index for a query expression. */ -public class QueryExpressionIndex extends Index implements SpatialIndex { +public abstract class QueryExpressionIndex extends Index { - private static final long MAX_AGE_NANOS = - TimeUnit.MILLISECONDS.toNanos(Constants.VIEW_COST_CACHE_MAX_AGE); + final QueryExpressionTable table; + final String querySQL; + final ArrayList originalParameters; + Query query; - private final QueryExpressionTable table; - private final String querySQL; - private final ArrayList originalParameters; - private boolean recursive; - private final int[] indexMasks; - private Query query; - private final SessionLocal createSession; - - /** - * The time in nanoseconds when this index (and its cost) was calculated. - */ - private final long evaluatedAt; - - /** - * Constructor for the original index in {@link TableView}. - * - * @param table the query expression table - * @param querySQL the query SQL - * @param originalParameters the original parameters - * @param recursive if the view is recursive - */ - public QueryExpressionIndex(QueryExpressionTable table, String querySQL, - ArrayList originalParameters, boolean recursive) { + QueryExpressionIndex(QueryExpressionTable table, String querySQL, ArrayList originalParameters) { super(table, 0, null, null, 0, IndexType.createNonUnique(false)); this.table = table; this.querySQL = querySQL; this.originalParameters = originalParameters; - this.recursive = recursive; columns = new Column[0]; - this.createSession = null; - this.indexMasks = null; - // this is a main index of TableView, it does not need eviction time - // stamp - evaluatedAt = Long.MIN_VALUE; } - /** - * Constructor for plan item generation. Over this index the query will be - * executed. - * - * @param table the query expression table - * @param index the main index - * @param session the session - * @param masks the masks - * @param filters table filters - * @param filter current filter - * @param sortOrder sort order - */ - public QueryExpressionIndex(QueryExpressionTable table, QueryExpressionIndex index, SessionLocal session, - int[] masks, TableFilter[] filters, int filter, SortOrder sortOrder) { - super(table, 0, null, null, 0, IndexType.createNonUnique(false)); - this.table = table; - this.querySQL = index.querySQL; - this.originalParameters = index.originalParameters; - this.recursive = index.recursive; - this.indexMasks = masks; - this.createSession = session; - columns = new Column[0]; - if (!recursive) { - query = getQuery(session, masks); - } - if (recursive || table.getTopQuery() != null) { - evaluatedAt = Long.MAX_VALUE; - } else { - long time = System.nanoTime(); - if (time == Long.MAX_VALUE) { - time++; - } - evaluatedAt = time; - } - } - - public SessionLocal getSession() { - return createSession; - } - - public boolean isExpired() { - assert evaluatedAt != Long.MIN_VALUE : "must not be called for main index of TableView"; - return !recursive && table.getTopQuery() == null && System.nanoTime() - evaluatedAt > MAX_AGE_NANOS; - } + public abstract boolean isExpired(); @Override public String getPlanSQL() { return query == null ? null : query.getPlanSQL(TRACE_SQL_FLAGS | ADD_PLAN_INFORMATION); } + public Query getQuery() { + return query; + } + @Override public void close(SessionLocal session) { // nothing to do @@ -132,262 +51,27 @@ public void close(SessionLocal session) { @Override public void add(SessionLocal session, Row row) { - throw DbException.getUnsupportedException("VIEW"); + throw DbException.getUnsupportedException(getClass().getSimpleName() + ".add"); } @Override public void remove(SessionLocal session, Row row) { - throw DbException.getUnsupportedException("VIEW"); - } - - @Override - public double getCost(SessionLocal session, int[] masks, - TableFilter[] filters, int filter, SortOrder sortOrder, - AllColumnsForPlan allColumnsSet) { - return recursive ? 1000 : query.getCost(); - } - - @Override - public Cursor find(SessionLocal session, SearchRow first, SearchRow last) { - return find(session, first, last, null); - } - - @Override - public Cursor findByGeometry(SessionLocal session, SearchRow first, SearchRow last, SearchRow intersection) { - return find(session, first, last, intersection); - } - - private Cursor findRecursive(SearchRow first, SearchRow last) { - TableView view = (TableView) table; - ResultInterface recursiveResult = view.getRecursiveResult(); - if (recursiveResult != null) { - recursiveResult.reset(); - return new QueryExpressionCursor(this, recursiveResult, first, last); - } - if (query == null) { - Parser parser = new Parser(createSession); - parser.setRightsChecked(true); - parser.setSuppliedParameters(originalParameters); - parser.setQueryScope(table.getQueryScope()); - query = (Query) parser.prepare(querySQL); - query.setNeverLazy(true); - } - if (!query.isUnion()) { - throw DbException.get(ErrorCode.SYNTAX_ERROR_2, - "recursive queries without UNION"); - } - SelectUnion union = (SelectUnion) query; - Query left = union.getLeft(); - left.setNeverLazy(true); - // to ensure the last result is not closed - left.disableCache(); - ResultInterface resultInterface = left.query(0); - LocalResult localResult = union.getEmptyResult(); - // ensure it is not written to disk, - // because it is not closed normally - localResult.setMaxMemoryRows(Integer.MAX_VALUE); - while (resultInterface.next()) { - Value[] cr = resultInterface.currentRow(); - localResult.addRow(cr); - } - Query right = union.getRight(); - right.setNeverLazy(true); - resultInterface.reset(); - view.setRecursiveResult(resultInterface); - // to ensure the last result is not closed - right.disableCache(); - while (true) { - resultInterface = right.query(0); - if (!resultInterface.hasNext()) { - break; - } - while (resultInterface.next()) { - Value[] cr = resultInterface.currentRow(); - localResult.addRow(cr); - } - resultInterface.reset(); - view.setRecursiveResult(resultInterface); - } - view.setRecursiveResult(null); - localResult.done(); - return new QueryExpressionCursor(this, localResult, first, last); - } - - /** - * Set the query parameters. - * - * @param session the session - * @param first the lower bound - * @param last the upper bound - * @param intersection the intersection - */ - public void setupQueryParameters(SessionLocal session, SearchRow first, SearchRow last, - SearchRow intersection) { - ArrayList paramList = query.getParameters(); - if (originalParameters != null) { - for (Parameter orig : originalParameters) { - if (orig != null) { - int idx = orig.getIndex(); - Value value = orig.getValue(session); - setParameter(paramList, idx, value); - } - } - } - int len; - if (first != null) { - len = first.getColumnCount(); - } else if (last != null) { - len = last.getColumnCount(); - } else if (intersection != null) { - len = intersection.getColumnCount(); - } else { - len = 0; - } - int idx = table.getParameterOffset(originalParameters); - for (int i = 0; i < len; i++) { - int mask = indexMasks[i]; - if ((mask & IndexCondition.EQUALITY) != 0) { - setParameter(paramList, idx++, first.getValue(i)); - } - if ((mask & IndexCondition.START) != 0) { - setParameter(paramList, idx++, first.getValue(i)); - } - if ((mask & IndexCondition.END) != 0) { - setParameter(paramList, idx++, last.getValue(i)); - } - if ((mask & IndexCondition.SPATIAL_INTERSECTS) != 0) { - setParameter(paramList, idx++, intersection.getValue(i)); - } - } - } - - private Cursor find(SessionLocal session, SearchRow first, SearchRow last, - SearchRow intersection) { - if (recursive) { - return findRecursive(first, last); - } - setupQueryParameters(session, first, last, intersection); - ResultInterface result = query.query(0); - return new QueryExpressionCursor(this, result, first, last); - } - - private static void setParameter(ArrayList paramList, int x, - Value v) { - if (x >= paramList.size()) { - // the parameter may be optimized away as in - // select * from (select null as x) where x=1; - return; - } - Parameter param = paramList.get(x); - param.setValue(v); - } - - public Query getQuery() { - return query; - } - - private Query getQuery(SessionLocal session, int[] masks) { - Query q = session.prepareQueryExpression(querySQL, table.getQueryScope()); - if (masks == null || !q.allowGlobalConditions()) { - q.preparePlan(); - return q; - } - int firstIndexParam = table.getParameterOffset(originalParameters); - // the column index of each parameter - // (for example: paramColumnIndex {0, 0} mean - // param[0] is column 0, and param[1] is also column 0) - IntArray paramColumnIndex = new IntArray(); - int indexColumnCount = 0; - for (int i = 0; i < masks.length; i++) { - int mask = masks[i]; - if (mask == 0) { - continue; - } - indexColumnCount++; - // the number of parameters depends on the mask; - // for range queries it is 2: >= x AND <= y - // but bitMask could also be 7 (=, and <=, and >=) - int bitCount = Integer.bitCount(mask); - for (int j = 0; j < bitCount; j++) { - paramColumnIndex.add(i); - } - } - int len = paramColumnIndex.size(); - ArrayList columnList = new ArrayList<>(len); - for (int i = 0; i < len;) { - int idx = paramColumnIndex.get(i); - columnList.add(table.getColumn(idx)); - int mask = masks[idx]; - if ((mask & IndexCondition.EQUALITY) != 0) { - Parameter param = new Parameter(firstIndexParam + i); - q.addGlobalCondition(param, idx, Comparison.EQUAL_NULL_SAFE); - i++; - } - if ((mask & IndexCondition.START) != 0) { - Parameter param = new Parameter(firstIndexParam + i); - q.addGlobalCondition(param, idx, Comparison.BIGGER_EQUAL); - i++; - } - if ((mask & IndexCondition.END) != 0) { - Parameter param = new Parameter(firstIndexParam + i); - q.addGlobalCondition(param, idx, Comparison.SMALLER_EQUAL); - i++; - } - if ((mask & IndexCondition.SPATIAL_INTERSECTS) != 0) { - Parameter param = new Parameter(firstIndexParam + i); - q.addGlobalCondition(param, idx, Comparison.SPATIAL_INTERSECTS); - i++; - } - } - columns = columnList.toArray(new Column[0]); - - // reconstruct the index columns from the masks - this.indexColumns = new IndexColumn[indexColumnCount]; - this.columnIds = new int[indexColumnCount]; - for (int type = 0, indexColumnId = 0; type < 2; type++) { - for (int i = 0; i < masks.length; i++) { - int mask = masks[i]; - if (mask == 0) { - continue; - } - if (type == 0) { - if ((mask & IndexCondition.EQUALITY) == 0) { - // the first columns need to be equality conditions - continue; - } - } else { - if ((mask & IndexCondition.EQUALITY) != 0) { - // after that only range conditions - continue; - } - } - Column column = table.getColumn(i); - indexColumns[indexColumnId] = new IndexColumn(column); - columnIds[indexColumnId] = column.getColumnId(); - indexColumnId++; - } - } - String sql = q.getPlanSQL(DEFAULT_SQL_FLAGS); - if (!sql.equals(querySQL)) { - q = session.prepareQueryExpression(sql, table.getQueryScope()); - } - q.preparePlan(); - return q; + throw DbException.getUnsupportedException(getClass().getSimpleName() + ".remove"); } @Override public void remove(SessionLocal session) { - throw DbException.getUnsupportedException("VIEW"); + throw DbException.getUnsupportedException(getClass().getSimpleName() + ".remove"); } @Override public void truncate(SessionLocal session) { - throw DbException.getUnsupportedException("VIEW"); + throw DbException.getUnsupportedException(getClass().getSimpleName() + ".truncate"); } @Override public void checkRename() { - throw DbException.getUnsupportedException("VIEW"); + throw DbException.getUnsupportedException(getClass().getSimpleName() + ".checkRename"); } @Override @@ -395,21 +79,14 @@ public boolean needRebuild() { return false; } - public void setRecursive(boolean value) { - this.recursive = value; - } - @Override public long getRowCount(SessionLocal session) { - return 0; + return 0L; } @Override public long getRowCountApproximation(SessionLocal session) { - return 0; + return 0L; } - public boolean isRecursive() { - return recursive; - } } diff --git a/h2/src/main/org/h2/index/RecursiveIndex.java b/h2/src/main/org/h2/index/RecursiveIndex.java new file mode 100644 index 0000000000..6b1a10ad03 --- /dev/null +++ b/h2/src/main/org/h2/index/RecursiveIndex.java @@ -0,0 +1,119 @@ +/* + * Copyright 2004-2024 H2 Group. Multiple-Licensed under the MPL 2.0, + * and the EPL 1.0 (https://h2database.com/html/license.html). + * Initial Developer: H2 Group + */ +package org.h2.index; + +import java.util.ArrayList; + +import org.h2.api.ErrorCode; +import org.h2.command.Parser; +import org.h2.command.query.AllColumnsForPlan; +import org.h2.command.query.Query; +import org.h2.command.query.SelectUnion; +import org.h2.engine.SessionLocal; +import org.h2.expression.Parameter; +import org.h2.message.DbException; +import org.h2.result.LocalResult; +import org.h2.result.ResultInterface; +import org.h2.result.SearchRow; +import org.h2.result.SortOrder; +import org.h2.table.CTE; +import org.h2.table.QueryExpressionTable; +import org.h2.table.TableFilter; +import org.h2.value.Value; + +/** + * A recursive index. + */ +public final class RecursiveIndex extends QueryExpressionIndex { + + private final SessionLocal createSession; + + /** + * Creates a new instance of a recursive index. + * + * @param table + * the query expression table + * @param querySQL + * the query SQL + * @param originalParameters + * the original parameters + * @param session + * the session + */ + public RecursiveIndex(QueryExpressionTable table, String querySQL, ArrayList originalParameters, + SessionLocal session) { + super(table, querySQL, originalParameters); + this.createSession = session; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public double getCost(SessionLocal session, int[] masks, TableFilter[] filters, int filter, SortOrder sortOrder, + AllColumnsForPlan allColumnsSet) { + return 1000d; + } + + @Override + public Cursor find(SessionLocal session, SearchRow first, SearchRow last) { + CTE cte = (CTE) table; + ResultInterface recursiveResult = cte.getRecursiveResult(); + if (recursiveResult != null) { + recursiveResult.reset(); + return new QueryExpressionCursor(this, recursiveResult, first, last); + } + if (query == null) { + Parser parser = new Parser(createSession); + parser.setRightsChecked(true); + parser.setSuppliedParameters(originalParameters); + parser.setQueryScope(table.getQueryScope()); + query = (Query) parser.prepare(querySQL); + query.setNeverLazy(true); + } + if (!query.isUnion()) { + throw DbException.get(ErrorCode.SYNTAX_ERROR_2, "recursive queries without UNION"); + } + SelectUnion union = (SelectUnion) query; + Query left = union.getLeft(); + left.setNeverLazy(true); + // to ensure the last result is not closed + left.disableCache(); + ResultInterface resultInterface = left.query(0); + LocalResult localResult = union.getEmptyResult(); + // ensure it is not written to disk, + // because it is not closed normally + localResult.setMaxMemoryRows(Integer.MAX_VALUE); + while (resultInterface.next()) { + Value[] cr = resultInterface.currentRow(); + localResult.addRow(cr); + } + Query right = union.getRight(); + right.setNeverLazy(true); + resultInterface.reset(); + cte.setRecursiveResult(resultInterface); + // to ensure the last result is not closed + right.disableCache(); + while (true) { + resultInterface = right.query(0); + if (!resultInterface.hasNext()) { + break; + } + while (resultInterface.next()) { + Value[] cr = resultInterface.currentRow(); + localResult.addRow(cr); + } + resultInterface.reset(); + cte.setRecursiveResult(resultInterface); + } + cte.setRecursiveResult(null); + localResult.done(); + return new QueryExpressionCursor(this, localResult, first, last); + } + +} diff --git a/h2/src/main/org/h2/index/RegularQueryExpressionIndex.java b/h2/src/main/org/h2/index/RegularQueryExpressionIndex.java new file mode 100644 index 0000000000..d0b73d4660 --- /dev/null +++ b/h2/src/main/org/h2/index/RegularQueryExpressionIndex.java @@ -0,0 +1,220 @@ +/* + * Copyright 2004-2024 H2 Group. Multiple-Licensed under the MPL 2.0, + * and the EPL 1.0 (https://h2database.com/html/license.html). + * Initial Developer: H2 Group + */ +package org.h2.index; + +import java.util.ArrayList; + +import org.h2.command.query.AllColumnsForPlan; +import org.h2.command.query.Query; +import org.h2.engine.Constants; +import org.h2.engine.SessionLocal; +import org.h2.expression.Parameter; +import org.h2.expression.condition.Comparison; +import org.h2.result.SearchRow; +import org.h2.result.SortOrder; +import org.h2.table.Column; +import org.h2.table.IndexColumn; +import org.h2.table.QueryExpressionTable; +import org.h2.table.TableFilter; +import org.h2.util.IntArray; +import org.h2.value.Value; + +/** + * A regular query expression index. + */ +public final class RegularQueryExpressionIndex extends QueryExpressionIndex implements SpatialIndex { + + private final int[] indexMasks; + + /** + * The time in nanoseconds when this index (and its cost) was calculated. + */ + private final long evaluatedAt; + + /** + * Creates a new instance of a regular query expression index. + * + * @param table + * the query expression table + * @param querySQL + * the query SQL + * @param originalParameters + * the original parameters + * @param session + * the session + * @param masks + * the masks + */ + public RegularQueryExpressionIndex(QueryExpressionTable table, String querySQL, + ArrayList originalParameters, SessionLocal session, int[] masks) { + super(table, querySQL, originalParameters); + indexMasks = masks; + Query q = session.prepareQueryExpression(querySQL, table.getQueryScope()); + if (masks != null && q.allowGlobalConditions()) { + q = addConditions(table, querySQL, originalParameters, session, masks, q); + } + q.preparePlan(); + query = q; + evaluatedAt = table.getTopQuery() == null ? System.nanoTime() : 0L; + } + + private Query addConditions(QueryExpressionTable table, String querySQL, ArrayList originalParameters, + SessionLocal session, int[] masks, Query q) { + int firstIndexParam = table.getParameterOffset(originalParameters); + // the column index of each parameter + // (for example: paramColumnIndex {0, 0} mean + // param[0] is column 0, and param[1] is also column 0) + IntArray paramColumnIndex = new IntArray(); + int indexColumnCount = 0; + for (int i = 0; i < masks.length; i++) { + int mask = masks[i]; + if (mask == 0) { + continue; + } + indexColumnCount++; + // the number of parameters depends on the mask; + // for range queries it is 2: >= x AND <= y + // but bitMask could also be 7 (=, and <=, and >=) + int bitCount = Integer.bitCount(mask); + for (int j = 0; j < bitCount; j++) { + paramColumnIndex.add(i); + } + } + int len = paramColumnIndex.size(); + ArrayList columnList = new ArrayList<>(len); + for (int i = 0; i < len;) { + int idx = paramColumnIndex.get(i); + columnList.add(table.getColumn(idx)); + int mask = masks[idx]; + if ((mask & IndexCondition.EQUALITY) != 0) { + Parameter param = new Parameter(firstIndexParam + i); + q.addGlobalCondition(param, idx, Comparison.EQUAL_NULL_SAFE); + i++; + } + if ((mask & IndexCondition.START) != 0) { + Parameter param = new Parameter(firstIndexParam + i); + q.addGlobalCondition(param, idx, Comparison.BIGGER_EQUAL); + i++; + } + if ((mask & IndexCondition.END) != 0) { + Parameter param = new Parameter(firstIndexParam + i); + q.addGlobalCondition(param, idx, Comparison.SMALLER_EQUAL); + i++; + } + if ((mask & IndexCondition.SPATIAL_INTERSECTS) != 0) { + Parameter param = new Parameter(firstIndexParam + i); + q.addGlobalCondition(param, idx, Comparison.SPATIAL_INTERSECTS); + i++; + } + } + columns = columnList.toArray(new Column[0]); + + // reconstruct the index columns from the masks + this.indexColumns = new IndexColumn[indexColumnCount]; + this.columnIds = new int[indexColumnCount]; + for (int type = 0, indexColumnId = 0; type < 2; type++) { + for (int i = 0; i < masks.length; i++) { + int mask = masks[i]; + if (mask == 0) { + continue; + } + if (type == 0) { + if ((mask & IndexCondition.EQUALITY) == 0) { + // the first columns need to be equality conditions + continue; + } + } else { + if ((mask & IndexCondition.EQUALITY) != 0) { + // after that only range conditions + continue; + } + } + Column column = table.getColumn(i); + indexColumns[indexColumnId] = new IndexColumn(column); + columnIds[indexColumnId] = column.getColumnId(); + indexColumnId++; + } + } + String sql = q.getPlanSQL(DEFAULT_SQL_FLAGS); + if (!sql.equals(querySQL)) { + q = session.prepareQueryExpression(sql, table.getQueryScope()); + } + return q; + } + + @Override + public boolean isExpired() { + return table.getTopQuery() == null + && System.nanoTime() - evaluatedAt > Constants.VIEW_COST_CACHE_MAX_AGE * 1_000_000L; + } + + @Override + public double getCost(SessionLocal session, int[] masks, TableFilter[] filters, int filter, SortOrder sortOrder, + AllColumnsForPlan allColumnsSet) { + return query.getCost(); + } + + @Override + public Cursor find(SessionLocal session, SearchRow first, SearchRow last) { + return find(session, first, last, null); + } + + @Override + public Cursor findByGeometry(SessionLocal session, SearchRow first, SearchRow last, SearchRow intersection) { + return find(session, first, last, intersection); + } + + private Cursor find(SessionLocal session, SearchRow first, SearchRow last, SearchRow intersection) { + ArrayList paramList = query.getParameters(); + if (originalParameters != null) { + for (Parameter orig : originalParameters) { + if (orig != null) { + int idx = orig.getIndex(); + Value value = orig.getValue(session); + setParameter(paramList, idx, value); + } + } + } + int len; + if (first != null) { + len = first.getColumnCount(); + } else if (last != null) { + len = last.getColumnCount(); + } else if (intersection != null) { + len = intersection.getColumnCount(); + } else { + len = 0; + } + int idx = table.getParameterOffset(originalParameters); + for (int i = 0; i < len; i++) { + int mask = indexMasks[i]; + if ((mask & IndexCondition.EQUALITY) != 0) { + setParameter(paramList, idx++, first.getValue(i)); + } + if ((mask & IndexCondition.START) != 0) { + setParameter(paramList, idx++, first.getValue(i)); + } + if ((mask & IndexCondition.END) != 0) { + setParameter(paramList, idx++, last.getValue(i)); + } + if ((mask & IndexCondition.SPATIAL_INTERSECTS) != 0) { + setParameter(paramList, idx++, intersection.getValue(i)); + } + } + return new QueryExpressionCursor(this, query.query(0), first, last); + } + + private static void setParameter(ArrayList paramList, int x, Value v) { + if (x >= paramList.size()) { + // the parameter may be optimized away as in + // select * from (select null as x) where x=1; + return; + } + Parameter param = paramList.get(x); + param.setValue(v); + } + +} diff --git a/h2/src/main/org/h2/table/CTE.java b/h2/src/main/org/h2/table/CTE.java new file mode 100644 index 0000000000..74a37a8901 --- /dev/null +++ b/h2/src/main/org/h2/table/CTE.java @@ -0,0 +1,108 @@ +/* + * Copyright 2004-2024 H2 Group. Multiple-Licensed under the MPL 2.0, + * and the EPL 1.0 (https://h2database.com/html/license.html). + * Initial Developer: H2 Group + */ +package org.h2.table; + +import java.util.ArrayList; + +import org.h2.command.QueryScope; +import org.h2.command.query.Query; +import org.h2.engine.SessionLocal; +import org.h2.expression.Parameter; +import org.h2.index.QueryExpressionIndex; +import org.h2.index.RecursiveIndex; +import org.h2.index.RegularQueryExpressionIndex; +import org.h2.result.ResultInterface; +import org.h2.util.ParserUtil; + +/** + * A common table expression. + */ +public final class CTE extends QueryExpressionTable { + + private final String querySQL; + private final boolean recursive; + private final QueryScope queryScope; + private final ArrayList originalParameters; + + private ResultInterface recursiveResult; + + public CTE(String name, Query query, String querySQL, ArrayList params, Column[] columnTemplates, + SessionLocal session, boolean recursive, QueryScope queryScope) { + super(session.getDatabase().getMainSchema(), 0, name); + setTemporary(true); + this.queryScope = queryScope; + this.querySQL = querySQL; + this.recursive = recursive; + this.originalParameters = params; + tables = new ArrayList<>(query.getTables()); + setColumns(initColumns(session, columnTemplates, query, false)); + viewQuery = query; + } + + @Override + protected QueryExpressionIndex createIndex(SessionLocal session, int[] masks) { + return recursive ? new RecursiveIndex(this, querySQL, originalParameters, session) + : new RegularQueryExpressionIndex(this, querySQL, originalParameters, session, masks); + } + + @Override + public Query getTopQuery() { + return null; + } + + @Override + public String getCreateSQL() { + return null; + } + + @Override + public boolean canDrop() { + return false; + } + + @Override + public TableType getTableType() { + return null; + } + + @Override + public StringBuilder getSQL(StringBuilder builder, int sqlFlags) { + return ParserUtil.quoteIdentifier(builder, getName(), sqlFlags); + } + + public String getQuerySQL() { + return querySQL; + } + + @Override + public QueryScope getQueryScope() { + return queryScope; + } + + public boolean isRecursive() { + return recursive; + } + + @Override + public boolean isDeterministic() { + if (recursive) { + return false; + } + return super.isDeterministic(); + } + + public void setRecursiveResult(ResultInterface value) { + if (recursiveResult != null) { + recursiveResult.close(); + } + this.recursiveResult = value; + } + + public ResultInterface getRecursiveResult() { + return recursiveResult; + } + +} diff --git a/h2/src/main/org/h2/table/DerivedTable.java b/h2/src/main/org/h2/table/DerivedTable.java index 99152746d8..4cb7f87406 100644 --- a/h2/src/main/org/h2/table/DerivedTable.java +++ b/h2/src/main/org/h2/table/DerivedTable.java @@ -14,6 +14,7 @@ import org.h2.expression.ExpressionVisitor; import org.h2.expression.Parameter; import org.h2.index.QueryExpressionIndex; +import org.h2.index.RegularQueryExpressionIndex; import org.h2.message.DbException; import org.h2.util.StringUtils; @@ -22,9 +23,11 @@ */ public final class DerivedTable extends QueryExpressionTable { - private String querySQL; + private final String querySQL; - private Query topQuery; + private final Query topQuery; + + private final ArrayList originalParameters; /** * Create a derived table out of the given query. @@ -42,8 +45,7 @@ public DerivedTable(SessionLocal session, String name, Column[] columnTemplates, query.prepareExpressions(); try { this.querySQL = query.getPlanSQL(DEFAULT_SQL_FLAGS); - ArrayList params = query.getParameters(); - index = new QueryExpressionIndex(this, querySQL, params, false); + originalParameters = query.getParameters(); tables = new ArrayList<>(query.getTables()); setColumns(initColumns(session, columnTemplates, query, true)); viewQuery = query; @@ -56,6 +58,11 @@ public DerivedTable(SessionLocal session, String name, Column[] columnTemplates, } } + @Override + protected QueryExpressionIndex createIndex(SessionLocal session, int[] masks) { + return new RegularQueryExpressionIndex(this, querySQL, originalParameters, session, masks); + } + @Override public boolean isQueryComparable() { if (!super.isQueryComparable()) { diff --git a/h2/src/main/org/h2/table/QueryExpressionTable.java b/h2/src/main/org/h2/table/QueryExpressionTable.java index cc30862773..8c93e6be9a 100644 --- a/h2/src/main/org/h2/table/QueryExpressionTable.java +++ b/h2/src/main/org/h2/table/QueryExpressionTable.java @@ -26,7 +26,6 @@ import org.h2.result.Row; import org.h2.result.SortOrder; import org.h2.schema.Schema; -import org.h2.util.StringUtils; import org.h2.value.TypeInfo; import org.h2.value.Value; @@ -88,19 +87,11 @@ public boolean equals(Object obj) { * clause overriding usual select names) * @param theQuery * - the query object we want the column list for - * @param querySQLOutput - * - array of length 1 to receive extra 'output' field in - * addition to return value - containing the SQL query of the - * Query object * @return a list of column object returned by withQuery */ - public static List createQueryColumnTemplateList(String[] cols, Query theQuery, String[] querySQLOutput) { + public static List createQueryColumnTemplateList(String[] cols, Query theQuery) { ArrayList columnTemplateList = new ArrayList<>(); theQuery.prepare(); - // String array of length 1 is to receive extra 'output' field in - // addition to - // return value - querySQLOutput[0] = StringUtils.cache(theQuery.getPlanSQL(ADD_PLAN_INFORMATION)); SessionLocal session = theQuery.getSession(); ArrayList withExpressions = theQuery.getExpressions(); for (int i = 0; i < withExpressions.size(); ++i) { @@ -126,8 +117,6 @@ static int getMaxParameterIndex(ArrayList parameters) { Query viewQuery; - QueryExpressionIndex index; - ArrayList tables; private long lastModificationCheck; @@ -190,7 +179,7 @@ public final PlanItem getBestPlanItem(SessionLocal session, int[] masks, TableFi Map indexCache = session.getViewIndexCache(getTableType() == null); QueryExpressionIndex i = indexCache.get(cacheKey); if (i == null || i.isExpired()) { - i = new QueryExpressionIndex(this, index, session, masks, filters, filter, sortOrder); + i = createIndex(session, masks); indexCache.put(cacheKey, i); } PlanItem item = new PlanItem(); @@ -199,6 +188,8 @@ public final PlanItem getBestPlanItem(SessionLocal session, int[] masks, TableFi return item; } + abstract QueryExpressionIndex createIndex(SessionLocal session, int[] masks); + @Override public boolean isQueryComparable() { for (Table t : tables) { @@ -241,7 +232,6 @@ public final long getRowCount(SessionLocal session) { @Override public final boolean canGetRowCount(SessionLocal session) { - // TODO could get the row count, but not that easy return false; } diff --git a/h2/src/main/org/h2/table/Table.java b/h2/src/main/org/h2/table/Table.java index 7fa142422e..52d06aa95d 100644 --- a/h2/src/main/org/h2/table/Table.java +++ b/h2/src/main/org/h2/table/Table.java @@ -104,7 +104,6 @@ public abstract class Table extends SchemaObject { private boolean onCommitDrop, onCommitTruncate; private volatile Row nullRow; private RowFactory rowFactory = RowFactory.getRowFactory(); - private boolean tableExpression; protected Table(Schema schema, int id, String name, boolean persistIndexes, boolean persistData) { super(schema, id, name, Trace.TABLE); @@ -1454,14 +1453,6 @@ public boolean isRowLockable() { return false; } - public void setTableExpression(boolean tableExpression) { - this.tableExpression = tableExpression; - } - - public boolean isTableExpression() { - return tableExpression; - } - /** * Return list of triggers. * diff --git a/h2/src/main/org/h2/table/TableFilter.java b/h2/src/main/org/h2/table/TableFilter.java index a603e1d356..765c0df036 100644 --- a/h2/src/main/org/h2/table/TableFilter.java +++ b/h2/src/main/org/h2/table/TableFilter.java @@ -768,11 +768,7 @@ public StringBuilder getPlanSQL(StringBuilder builder, boolean isJoin, int sqlFl } return builder; } - if (table instanceof TableView && ((TableView) table).isTableExpression()) { - ParserUtil.quoteIdentifier(builder, table.getName(), sqlFlags); - } else { - table.getSQL(builder, sqlFlags); - } + table.getSQL(builder, sqlFlags); if (table instanceof TableView && ((TableView) table).isInvalid()) { throw DbException.get(ErrorCode.VIEW_IS_INVALID_2, table.getName(), "not compiled"); } diff --git a/h2/src/main/org/h2/table/TableView.java b/h2/src/main/org/h2/table/TableView.java index 5fa613f5d0..4f10d47673 100644 --- a/h2/src/main/org/h2/table/TableView.java +++ b/h2/src/main/org/h2/table/TableView.java @@ -15,11 +15,10 @@ import org.h2.command.query.Query; import org.h2.engine.Database; import org.h2.engine.SessionLocal; -import org.h2.expression.Parameter; import org.h2.index.Index; import org.h2.index.QueryExpressionIndex; +import org.h2.index.RegularQueryExpressionIndex; import org.h2.message.DbException; -import org.h2.result.ResultInterface; import org.h2.result.SortOrder; import org.h2.schema.Schema; import org.h2.util.StringUtils; @@ -34,21 +33,17 @@ public final class TableView extends QueryExpressionTable { private String querySQL; private Column[] columnTemplates; - private boolean allowRecursive; private DbException createException; - private ResultInterface recursiveResult; - private boolean isRecursiveQueryDetected; - private boolean isTableExpression; - private QueryScope queryScope; - - public TableView(Schema schema, int id, String name, String querySQL, - ArrayList params, Column[] columnTemplates, SessionLocal session, - boolean allowRecursive, boolean literalsChecked, boolean isTableExpression, boolean isTemporary, - QueryScope queryScope) { + + public TableView(Schema schema, int id, String name, String querySQL, Column[] columnTemplates, + SessionLocal session) { super(schema, id, name); - this.queryScope = queryScope; - setTemporary(isTemporary); - init(querySQL, params, columnTemplates, session, allowRecursive, literalsChecked, isTableExpression); + init(querySQL, columnTemplates, session); + } + + @Override + protected QueryExpressionIndex createIndex(SessionLocal session, int[] masks) { + return new RegularQueryExpressionIndex(this, querySQL, null, session, masks); } /** @@ -58,54 +53,38 @@ public TableView(Schema schema, int id, String name, String querySQL, * @param querySQL the SQL statement * @param newColumnTemplates the columns * @param session the session - * @param recursive whether this is a recursive view * @param force if errors should be ignored - * @param literalsChecked if literals have been checked */ - public void replace(String querySQL, Column[] newColumnTemplates, SessionLocal session, - boolean recursive, boolean force, boolean literalsChecked) { + public void replace(String querySQL, Column[] newColumnTemplates, SessionLocal session, boolean force) { String oldQuerySQL = this.querySQL; Column[] oldColumnTemplates = this.columnTemplates; - boolean oldRecursive = this.allowRecursive; - init(querySQL, null, newColumnTemplates, session, recursive, literalsChecked, isTableExpression); + init(querySQL, newColumnTemplates, session); DbException e = recompile(session, force, true); if (e != null) { - init(oldQuerySQL, null, oldColumnTemplates, session, oldRecursive, - literalsChecked, isTableExpression); + init(oldQuerySQL, oldColumnTemplates, session); recompile(session, true, false); throw e; } } - private synchronized void init(String querySQL, ArrayList params, - Column[] columnTemplates, SessionLocal session, boolean allowRecursive, boolean literalsChecked, - boolean isTableExpression) { + private synchronized void init(String querySQL, Column[] columnTemplates, SessionLocal session) { this.querySQL = querySQL; this.columnTemplates = columnTemplates; - this.allowRecursive = allowRecursive; - this.isRecursiveQueryDetected = false; - this.isTableExpression = isTableExpression; - index = new QueryExpressionIndex(this, querySQL, params, allowRecursive); - initColumnsAndTables(session, literalsChecked); + initColumnsAndTables(session); } - private Query compileViewQuery(SessionLocal session, String sql, boolean literalsChecked) { + private static Query compileViewQuery(SessionLocal session, String sql) { Prepared p; session.setParsingCreateView(true); try { - p = session.prepare(sql, false, literalsChecked, queryScope); + p = session.prepare(sql, false, false, null); } finally { session.setParsingCreateView(false); } if (!(p instanceof Query)) { throw DbException.getSyntaxError(sql, 0); } - Query q = (Query) p; - // only potentially recursive cte queries need to be non-lazy - if (isTableExpression && allowRecursive) { - q.setNeverLazy(true); - } - return q; + return (Query) p; } /** @@ -117,17 +96,16 @@ private Query compileViewQuery(SessionLocal session, String sql, boolean literal * @return the exception if re-compiling this or any dependent view failed * (only when force is disabled) */ - public synchronized DbException recompile(SessionLocal session, boolean force, - boolean clearIndexCache) { + public synchronized DbException recompile(SessionLocal session, boolean force, boolean clearIndexCache) { try { - compileViewQuery(session, querySQL, false); + compileViewQuery(session, querySQL); } catch (DbException e) { if (!force) { return e; } } ArrayList dependentViews = new ArrayList<>(getDependentViews()); - initColumnsAndTables(session, false); + initColumnsAndTables(session); for (TableView v : dependentViews) { DbException e = v.recompile(session, force, false); if (e != null && !force) { @@ -140,12 +118,11 @@ public synchronized DbException recompile(SessionLocal session, boolean force, return force ? null : createException; } - private void initColumnsAndTables(SessionLocal session, boolean literalsChecked) { + private void initColumnsAndTables(SessionLocal session) { Column[] cols; removeCurrentViewFromOtherTables(); - setTableExpression(isTableExpression); try { - Query compiledQuery = compileViewQuery(session, querySQL, literalsChecked); + Query compiledQuery = compileViewQuery(session, querySQL); this.querySQL = compiledQuery.getPlanSQL(DEFAULT_SQL_FLAGS); tables = new ArrayList<>(compiledQuery.getTables()); cols = initColumns(session, columnTemplates, compiledQuery, false); @@ -160,21 +137,8 @@ private void initColumnsAndTables(SessionLocal session, boolean literalsChecked) // If it can't be compiled, then it's a 'zero column table' // this avoids problems when creating the view when opening the // database. - // If it can not be compiled - it could also be a recursive common - // table expression query. - if (isRecursiveQueryExceptionDetected(createException)) { - this.isRecursiveQueryDetected = true; - } tables = Utils.newSmallArrayList(); cols = new Column[0]; - if (allowRecursive && columnTemplates != null) { - cols = new Column[columnTemplates.length]; - for (int i = 0; i < columnTemplates.length; i++) { - cols[i] = columnTemplates[i].getClone(); - } - index.setRecursive(true); - createException = null; - } } setColumns(cols); if (getId() != 0) { @@ -232,9 +196,6 @@ private String getCreateSQL(boolean orReplace, boolean force, String quotedName) builder.append("FORCE "); } builder.append("VIEW "); - if (isTableExpression) { - builder.append("TABLE_EXPRESSION "); - } builder.append(quotedName); if (comment != null) { builder.append(" COMMENT "); @@ -267,7 +228,6 @@ public void removeChildrenAndResources(SessionLocal session) { removeCurrentViewFromOtherTables(); super.removeChildrenAndResources(session); querySQL = null; - index = null; clearIndexCaches(database); invalidate(); } @@ -283,22 +243,13 @@ public static void clearIndexCaches(Database database) { } } - @Override - public StringBuilder getSQL(StringBuilder builder, int sqlFlags) { - if (isTemporary() && querySQL != null) { - builder.append("(\n"); - return StringUtils.indent(builder, querySQL, 4, true).append(')'); - } - return super.getSQL(builder, sqlFlags); - } - public String getQuerySQL() { return querySQL; } @Override public QueryScope getQueryScope() { - return queryScope; + return null; } @Override @@ -335,55 +286,14 @@ private void addDependentViewToTables() { } } - public boolean isRecursive() { - return allowRecursive; - } - @Override public boolean isDeterministic() { - if (allowRecursive || viewQuery == null) { + if (viewQuery == null) { return false; } return super.isDeterministic(); } - public void setRecursiveResult(ResultInterface value) { - if (recursiveResult != null) { - recursiveResult.close(); - } - this.recursiveResult = value; - } - - public ResultInterface getRecursiveResult() { - return recursiveResult; - } - - /** - * Was query recursion detected during compiling. - * - * @return true if yes - */ - public boolean isRecursiveQueryDetected() { - return isRecursiveQueryDetected; - } - - /** - * Does exception indicate query recursion? - */ - private boolean isRecursiveQueryExceptionDetected(DbException exception) { - if (exception == null) { - return false; - } - int errorCode = exception.getErrorCode(); - if (errorCode != ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1 && - errorCode != ErrorCode.TABLE_OR_VIEW_NOT_FOUND_DATABASE_EMPTY_1 && - errorCode != ErrorCode.TABLE_OR_VIEW_NOT_FOUND_WITH_CANDIDATES_2 - ) { - return false; - } - return exception.getMessage().contains("\"" + this.getName() + "\""); - } - public List
getTables() { return tables; } diff --git a/h2/src/test/org/h2/test/db/TestGeneralCommonTableQueries.java b/h2/src/test/org/h2/test/db/TestGeneralCommonTableQueries.java index e7833637aa..9923fc1865 100644 --- a/h2/src/test/org/h2/test/db/TestGeneralCommonTableQueries.java +++ b/h2/src/test/org/h2/test/db/TestGeneralCommonTableQueries.java @@ -508,7 +508,7 @@ private void testSimple2By4RowRecursiveQuery() throws Exception { String[] expectedColumnNames = new String[]{"K", "N", "N2"}; String setupSQL = "-- do nothing"; - String withQuery = "with \n"+ + String withQuery = "with recursive\n"+ "r1(n,k) as ((select 1, 0) union all (select n+1,k+1 from r1 where n <= 3)),"+ "r2(n,k) as ((select 10,0) union all (select n+1,k+1 from r2 where n <= 13))"+ "select r1.k, r1.n, r2.n AS n2 from r1 inner join r2 ON r1.k= r2.k "; diff --git a/h2/src/test/org/h2/test/db/TestPersistentCommonTableExpressions.java b/h2/src/test/org/h2/test/db/TestPersistentCommonTableExpressions.java index 71793170a5..5c3441909e 100644 --- a/h2/src/test/org/h2/test/db/TestPersistentCommonTableExpressions.java +++ b/h2/src/test/org/h2/test/db/TestPersistentCommonTableExpressions.java @@ -103,7 +103,7 @@ private void testPersistentRecursiveTableInCreateView() throws Exception { +" FROM my_tree mt \n" +"INNER JOIN tree_cte mtc ON mtc.child_fk = mt.parent_fk \n" +"), \n" - +"unused_cte AS ( SELECT 1 AS unUsedColumn ) \n" + +"unused_cte(unUsedColumn) AS ( SELECT 1 AS unUsedColumn ) \n" +"SELECT sub_tree_root_id, tree_level, parent_fk, child_fk FROM tree_cte; \n"; String withQuery = "SELECT * FROM v_my_tree"; @@ -227,7 +227,7 @@ private void testPersistentRecursiveTableInCreateViewDropAllObjects() throws Exc +" FROM my_tree mt \n" +"INNER JOIN tree_cte mtc ON mtc.child_fk = mt.parent_fk \n" +"), \n" - +"unused_cte AS ( SELECT 1 AS unUsedColumn ) \n" + +"unused_cte(unUsedColumn) AS ( SELECT 1 AS unUsedColumn ) \n" +"SELECT sub_tree_root_id, tree_level, parent_fk, child_fk FROM tree_cte; \n"; String withQuery = "SELECT * FROM v_my_tree"; diff --git a/h2/src/test/org/h2/test/db/TestRecursiveQueries.java b/h2/src/test/org/h2/test/db/TestRecursiveQueries.java index baf86d175c..27f2db4ac3 100644 --- a/h2/src/test/org/h2/test/db/TestRecursiveQueries.java +++ b/h2/src/test/org/h2/test/db/TestRecursiveQueries.java @@ -150,7 +150,7 @@ private void testSimpleUnionAll() throws Exception { null, null); rs = stat.executeQuery("select x from system_range(1,5) " - + "where x not in (with w(x) as (select 1 union all select x+1 from w where x<3) " + + "where x not in (with recursive w(x) as (select 1 union all select x+1 from w where x<3) " + "select x from w)"); assertResultSetOrdered(rs, new String[][]{{"4"}, {"5"}}); diff --git a/h2/src/test/org/h2/test/scripts/dml/with.sql b/h2/src/test/org/h2/test/scripts/dml/with.sql index a1388731d3..d76e917a3b 100644 --- a/h2/src/test/org/h2/test/scripts/dml/with.sql +++ b/h2/src/test/org/h2/test/scripts/dml/with.sql @@ -9,7 +9,7 @@ create table folder(id int primary key, name varchar(255), parent int); insert into folder values(1, null, null), (2, 'bin', 1), (3, 'docs', 1), (4, 'html', 3), (5, 'javadoc', 3), (6, 'ext', 1), (7, 'service', 1), (8, 'src', 1), (9, 'docsrc', 8), (10, 'installer', 8), (11, 'main', 8), (12, 'META-INF', 11), (13, 'org', 11), (14, 'h2', 13), (15, 'test', 8), (16, 'tools', 8); > update count: 16 -with link(id, name, level) as (select id, name, 0 from folder where parent is null union all select folder.id, ifnull(link.name || '/', '') || folder.name, level + 1 from link inner join folder on link.id = folder.parent) select name from link where name is not null order by cast(id as int); +with recursive link(id, name, level) as (select id, name, 0 from folder where parent is null union all select folder.id, ifnull(link.name || '/', '') || folder.name, level + 1 from link inner join folder on link.id = folder.parent) select name from link where name is not null order by cast(id as int); > NAME > ----------------- > bin @@ -36,13 +36,13 @@ explain with recursive r(n) as ( (select 1) union all (select n+1 from r where n < 3) ) select n from r; ->> WITH RECURSIVE "R"("N") AS ( (SELECT 1) UNION ALL (SELECT "N" + 1 FROM "R" /* table scan */ WHERE "N" < 3) ) SELECT "N" FROM "R" "R" /* null */ +>> WITH RECURSIVE "R"("N") AS ( (SELECT 1) UNION ALL (SELECT "N" + 1 FROM "R" WHERE "N" < 3) ) SELECT "N" FROM "R" "R" /* null */ explain with recursive "r"(n) as ( (select 1) union all (select n+1 from "r" where n < 3) ) select n from "r"; ->> WITH RECURSIVE "r"("N") AS ( (SELECT 1) UNION ALL (SELECT "N" + 1 FROM "r" /* table scan */ WHERE "N" < 3) ) SELECT "N" FROM "r" "r" /* null */ +>> WITH RECURSIVE "r"("N") AS ( (SELECT 1) UNION ALL (SELECT "N" + 1 FROM "r" WHERE "N" < 3) ) SELECT "N" FROM "r" "r" /* null */ select sum(n) from ( with recursive r(n) as ( @@ -80,7 +80,7 @@ select 0 from ( > - > rows: 0 -with +with recursive r0(n,k) as (select -1, 0), r1(n,k) as ((select 1, 0) union all (select n+1,k+1 from r1 where n <= 3)), r2(n,k) as ((select 10,0) union all (select n+1,k+1 from r2 where n <= 13)) @@ -93,12 +93,34 @@ with CREATE SCHEMA SCH; > ok -CREATE VIEW SCH.R2(N) AS +WITH RECURSIVE R1 AS ( +(SELECT 1) +UNION ALL +(SELECT (N + 1) FROM R1 WHERE N < 3)) +TABLE R1; +> exception SYNTAX_ERROR_2 + WITH R1(N) AS ( (SELECT 1) UNION ALL (SELECT (N + 1) FROM R1 WHERE N < 3)) TABLE R1; +> exception TABLE_OR_VIEW_NOT_FOUND_DATABASE_EMPTY_1 + +WITH RECURSIVE R1(A) AS (SELECT 1) +SELECT A FROM R1 WHERE A IN (WITH RECURSIVE R2(B) AS (SELECT 1) TABLE R2); +>> 1 + +WITH RECURSIVE R1(A) AS (WITH RECURSIVE R2(B) AS (SELECT 1) TABLE R2) +TABLE R1; +> exception SYNTAX_ERROR_2 + +CREATE VIEW SCH.R2(N) AS +WITH RECURSIVE R1(N) AS ( +(SELECT 1) +UNION ALL +(SELECT (N + 1) FROM R1 WHERE N < 3)) +TABLE R1; > ok SELECT * FROM SCH.R2; @@ -181,7 +203,7 @@ EXPLAIN WITH RECURSIVE V(V1, V2) AS ( SELECT V1, V2, COUNT(*) FROM V LEFT JOIN (SELECT T1 / T2 R FROM (VALUES (10, 0)) T(T1, T2) WHERE T2*T2*T2*T2*T2*T2 <> 0) X ON X.R > V.V1 AND X.R < V.V2 GROUP BY V1, V2; ->> WITH RECURSIVE "V"("V1", "V2") AS ( (SELECT 0 AS "V1", 1 AS "V2") UNION ALL (SELECT "V1" + 1, "V2" + 1 FROM "V" /* table scan */ WHERE "V2" < 10) ) SELECT "V1", "V2", COUNT(*) FROM "V" "V" /* null */ LEFT OUTER JOIN ( SELECT "T1" / "T2" AS "R" FROM (VALUES (10, 0)) "T"("T1", "T2") WHERE ((((("T2" * "T2") * "T2") * "T2") * "T2") * "T2") <> 0 ) "X" /* SELECT T1 / T2 AS R FROM (VALUES (10, 0)) T(T1, T2) /* table scan */ WHERE ((((((T2 * T2) * T2) * T2) * T2) * T2) <> 0) _LOCAL_AND_GLOBAL_ (((T1 / T2) >= ?1) AND ((T1 / T2) <= ?2)): R > V.V1 AND R < V.V2 */ ON ("X"."R" > "V"."V1") AND ("X"."R" < "V"."V2") GROUP BY "V1", "V2" +>> WITH RECURSIVE "V"("V1", "V2") AS ( (SELECT 0 AS "V1", 1 AS "V2") UNION ALL (SELECT "V1" + 1, "V2" + 1 FROM "V" WHERE "V2" < 10) ) SELECT "V1", "V2", COUNT(*) FROM "V" "V" /* null */ LEFT OUTER JOIN ( SELECT "T1" / "T2" AS "R" FROM (VALUES (10, 0)) "T"("T1", "T2") WHERE ((((("T2" * "T2") * "T2") * "T2") * "T2") * "T2") <> 0 ) "X" /* SELECT T1 / T2 AS R FROM (VALUES (10, 0)) T(T1, T2) /* table scan */ WHERE ((((((T2 * T2) * T2) * T2) * T2) * T2) <> 0) _LOCAL_AND_GLOBAL_ (((T1 / T2) >= ?1) AND ((T1 / T2) <= ?2)): R > V.V1 AND R < V.V2 */ ON ("X"."R" > "V"."V1") AND ("X"."R" < "V"."V2") GROUP BY "V1", "V2" -- Data change delta tables in WITH CREATE TABLE TEST("VALUE" INT NOT NULL PRIMARY KEY);