diff --git a/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java b/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java index 186209b94c..bc7c55bef3 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java @@ -12416,7 +12416,7 @@ final class DefaultParseContext extends AbstractScope implements ParseContext { } else { parse('*'); - return lookupTable(positionBeforeName, result == null ? i1 : DSL.name(result.toArray(EMPTY_NAME))).asterisk(); + return lookupQualifiedAsterisk(positionBeforeName, result == null ? i1 : DSL.name(result.toArray(EMPTY_NAME))); } } while (parseIf('.')); @@ -15004,10 +15004,11 @@ final class DefaultParseContext extends AbstractScope implements ParseContext { } private class ParseScope { - private boolean scopeClear = false; - private final ScopeStack> tableScope = new ScopeStack<>(); - private final ScopeStack> fieldScope = new ScopeStack<>(); - private final ScopeStack> lookupFields = new ScopeStack<>(); + private boolean scopeClear = false; + private final ScopeStack> tableScope = new ScopeStack<>(); + private final ScopeStack> fieldScope = new ScopeStack<>(); + private final ScopeStack lookupQualifiedAsterisks = new ScopeStack<>(); + private final ScopeStack> lookupFields = new ScopeStack<>(); @@ -15026,6 +15027,8 @@ final class DefaultParseContext extends AbstractScope implements ParseContext { private final void scopeResolve() { if (!lookupFields.isEmpty()) unknownField(lookupFields.iterator().next()); + if (!lookupQualifiedAsterisks.isEmpty()) + unknownTable(lookupQualifiedAsterisks.iterator().next()); } private final void scopeStart() { @@ -15033,10 +15036,28 @@ final class DefaultParseContext extends AbstractScope implements ParseContext { fieldScope.scopeStart(); lookupFields.scopeStart(); lookupFields.setAll(null); + lookupQualifiedAsterisks.scopeStart(); + lookupQualifiedAsterisks.setAll(null); } private final void scopeEnd(Query scopeOwner) { - List> retain = new ArrayList<>(); + List> fields = new ArrayList<>(); + + // [#14372] Avoid looking up tables at a higher scope level + lookupLoop: + for (QualifiedAsteriskProxy lookup : scope.lookupQualifiedAsterisks.iterableAtScopeLevel()) { + for (Table t : scope.tableScope) { + + // [#15056] TODO: Could there be an ambiguity, as with fields? + if (t.getName().equals(lookup.$table().getName())) { + lookup.delegate((QualifiedAsteriskImpl) t.asterisk()); + continue lookupLoop; + } + } + + // [#15056] TODO: Should we support references to higher scopes? + unknownTable(lookup); + } // [#14372] Avoid looking up fields at a higher scope level for (FieldProxy lookup : scope.lookupFields.iterableAtScopeLevel()) { @@ -15061,15 +15082,16 @@ final class DefaultParseContext extends AbstractScope implements ParseContext { } else { lookup.scopeOwner(scopeOwner); - retain.add(lookup); + fields.add(lookup); } } + scope.lookupQualifiedAsterisks.scopeEnd(); scope.lookupFields.scopeEnd(); scope.tableScope.scopeEnd(); scope.fieldScope.scopeEnd(); - for (FieldProxy r : retain) + for (FieldProxy r : fields) if (scope.lookupFields.get(r.getQualifiedName()) == null) if (scope.lookupFields.inScope()) scope.lookupFields.set(r.getQualifiedName(), r); @@ -15119,6 +15141,15 @@ final class DefaultParseContext extends AbstractScope implements ParseContext { } } } + + private final void unknownTable(QualifiedAsteriskProxy asterisk) { + if (!scopeClear) { + if (metaLookups() == THROW_ON_FAILURE) { + position(asterisk.position()); + throw exception("Unknown table identifier"); + } + } + } } private final Value> resolveInTableScope(Iterable>> tables, Name lookupName, FieldProxy lookup, Value> found) { @@ -15196,6 +15227,17 @@ final class DefaultParseContext extends AbstractScope implements ParseContext { return table(name); } + private final QualifiedAsterisk lookupQualifiedAsterisk(int positionBeforeName, Name name) { + if (metaLookups() == ParseWithMetaLookups.OFF || scope.lookupQualifiedAsterisks.scopeLevel() < 0) + return table(name).asterisk(); + + QualifiedAsteriskProxy asterisk = scope.lookupQualifiedAsterisks.get(name); + if (asterisk == null) + scope.lookupQualifiedAsterisks.set(name, asterisk = new QualifiedAsteriskProxy((QualifiedAsteriskImpl) table(name).asterisk(), positionBeforeName)); + + return asterisk; + } + private final Field lookupField(int positionBeforeName, Name name) { if (metaLookups() == ParseWithMetaLookups.OFF || scope.lookupFields.scopeLevel() < 0) return field(name); diff --git a/jOOQ/src/main/java/org/jooq/impl/QualifiedAsteriskProxy.java b/jOOQ/src/main/java/org/jooq/impl/QualifiedAsteriskProxy.java new file mode 100644 index 0000000000..719eb198c6 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/impl/QualifiedAsteriskProxy.java @@ -0,0 +1,210 @@ +/* + * Licensed 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 + * + * https://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. + * + * Other licenses: + * ----------------------------------------------------------------------------- + * Commercial licenses for this work are available. These replace the above + * ASL 2.0 and offer limited warranties, support, maintenance, and commercial + * database integrations. + * + * For more information, please visit: https://www.jooq.org/legal/licensing + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ +package org.jooq.impl; + +import org.jooq.Clause; +import org.jooq.Context; +import org.jooq.Field; +import org.jooq.Name; +import org.jooq.QualifiedAsterisk; +import org.jooq.Query; +import org.jooq.Table; +import org.jooq.impl.QOM.UProxy; +import org.jooq.impl.QOM.UnmodifiableList; + +/** + * A {@link Field} that acts as another field, allowing for the proxied field to + * be replaced. + * + * @author Lukas Eder + */ +final class QualifiedAsteriskProxy +extends + AbstractQueryPart +implements + QualifiedAsterisk, + UProxy, + ScopeMappable +{ + + /** + * The resolved expression after a successful meta lookup. + */ + private QualifiedAsteriskImpl delegate; + + /** + * The position in the parsed SQL string where this qualified asterisk proxy + * was encountered. + */ + private final int position; + + /** + * The scope owner that produced this qualified asterisk proxy. + */ + Query scopeOwner; + + /** + * Whether this qualified asterisk proxy could be resolved at some scope + * level. + */ + boolean resolved; + + QualifiedAsteriskProxy(QualifiedAsteriskImpl delegate, int position) { + this.delegate = delegate; + this.position = position; + } + + final int position() { + return position; + } + + final void delegate(QualifiedAsteriskImpl newDelegate) { + resolve(); + this.delegate = newDelegate; + } + + final QualifiedAsteriskProxy resolve() { + this.resolved = true; + this.scopeOwner = null; + + return this; + } + + final void scopeOwner(Query query) { + if (!resolved && scopeOwner == null) + scopeOwner = query; + } + + @Override + public final int hashCode() { + return delegate.hashCode(); + } + + @Override + public final boolean equals(Object that) { + return delegate.equals(that); + } + + @Override + public final boolean declaresFields() { + return delegate.declaresFields(); + } + + @Override + public final boolean declaresTables() { + return delegate.declaresTables(); + } + + @Override + public final boolean declaresWindows() { + return delegate.declaresWindows(); + } + + @Override + public final boolean declaresCTE() { + return delegate.declaresCTE(); + } + + @Override + public final boolean generatesCast() { + return delegate.generatesCast(); + } + + @Override + public final boolean rendersContent(Context ctx) { + return delegate.rendersContent(ctx); + } + + @Override + public final void accept(Context ctx) { + delegate.accept(ctx); + } + + @Override + public final Clause[] clauses(Context ctx) { + return delegate.clauses(ctx); + } + + @Override + public final String toString() { + return delegate.toString(); + } + + // ------------------------------------------------------------------------- + // XXX: DSL API + // ------------------------------------------------------------------------- + + @Override + public final Table qualifier() { + return delegate.qualifier(); + } + + @Override + public final QualifiedAsterisk except(String... fieldNames) { + return new QualifiedAsteriskProxy((QualifiedAsteriskImpl) delegate.except(fieldNames), position); + } + + @Override + public final QualifiedAsterisk except(Name... fieldNames) { + return new QualifiedAsteriskProxy((QualifiedAsteriskImpl) delegate.except(fieldNames), position); + } + + @Override + public final QualifiedAsterisk except(Field... fields) { + return new QualifiedAsteriskProxy((QualifiedAsteriskImpl) delegate.except(fields), position); + } + + // ------------------------------------------------------------------------- + // XXX: Query Object Model + // ------------------------------------------------------------------------- + + @Override + public final QualifiedAsterisk $delegate() { + return delegate; + } + + @Override + public final Table $table() { + return delegate.$table(); + } + + @Override + public final UnmodifiableList> $except() { + return delegate.$except(); + } +} \ No newline at end of file diff --git a/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java b/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java index 990adb6928..2dcfed30f9 100644 --- a/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java @@ -259,6 +259,7 @@ import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; +import org.jooq.Asterisk; import org.jooq.Clause; import org.jooq.CommonTableExpression; import org.jooq.Comparator; @@ -3866,7 +3867,7 @@ final class SelectQueryImpl extends AbstractResultQuery imp return getSelectResolveAllAsterisks(Tools.configuration(configuration()).dsl()); } - private final Collection> subtract(List> left, List> right) { + private static final Collection> subtract(List> left, List> right) { // [#7921] TODO Make this functionality more generally reusable FieldsImpl e = new FieldsImpl<>(right); @@ -3891,7 +3892,7 @@ final class SelectQueryImpl extends AbstractResultQuery imp */ final SelectFieldList getSelectResolveImplicitAsterisks() { if (getSelectAsSpecified().isEmpty()) - return resolveAsterisk(new SelectFieldList<>()); + return resolveAsterisk(new SelectFieldList()); return getSelectAsSpecified(); } @@ -3946,6 +3947,7 @@ final class SelectQueryImpl extends AbstractResultQuery imp return result; } + @SuppressWarnings("unchecked") private final void appendResolveSomeAsterisks0( Scope ctx, boolean resolveSupported, @@ -3958,31 +3960,31 @@ final class SelectQueryImpl extends AbstractResultQuery imp if (s instanceof Field f) { result.add(getResolveProjection(ctx, f)); } - else if (s instanceof QualifiedAsteriskImpl q) { + else if (s instanceof QualifiedAsterisk q) { // [#9743] Split join table asterisks if (q.qualifier() instanceof QOM.JoinTable j) { appendResolveSomeAsterisks0(ctx, resolveSupported, result, resolveExcept, resolveUnqualifiedCombined, list, j.$table1().asterisk()); appendResolveSomeAsterisks0(ctx, resolveSupported, result, resolveExcept, resolveUnqualifiedCombined, list, j.$table2().asterisk()); } - else if (q.fields.isEmpty()) + else if (q.$except().isEmpty()) if (resolveSupported) - result.addAll(Arrays.asList(q.qualifier().fields())); + result.addAll(asList(q.qualifier().fields())); else result.add(s); else if (resolveExcept) - result.addAll(subtract(Arrays.asList(((QualifiedAsterisk) s).qualifier().fields()), (((QualifiedAsteriskImpl) s).fields))); + result.addAll(subtract(asList(q.qualifier().fields()), q.$except())); else result.add(s); } - else if (s instanceof AsteriskImpl a) { - if (a.fields.isEmpty()) + else if (s instanceof Asterisk a) { + if (a.$except().isEmpty()) if (resolveSupported || resolveUnqualifiedCombined && list.size() > 1) result.addAll(resolveAsterisk(new QueryPartList<>())); else result.add(s); else if (resolveExcept) - result.addAll(resolveAsterisk(new QueryPartList<>(), a.fields)); + result.addAll(resolveAsterisk(new QueryPartList<>(), (QueryPartListView>) a.$except())); else result.add(s); } @@ -4046,7 +4048,7 @@ final class SelectQueryImpl extends AbstractResultQuery imp return resolveAsterisk(result, null); } - private final >> Q resolveAsterisk(Q result, QueryPartList> except) { + private final >> Q resolveAsterisk(Q result, QueryPartCollectionView> except) { FieldsImpl e = except == null ? null : new FieldsImpl<>(except); // [#109] [#489] [#7231]: SELECT * is only applied when at least one