DATAJDBC-309 - Reworked visitor structure towards a more modular design.

Also added support for conditions.

Original pull request: #119.
This commit is contained in:
Jens Schauder
2019-01-25 12:30:25 +01:00
committed by Mark Paluch
parent 10b38c5043
commit 1c9fad853f
35 changed files with 985 additions and 908 deletions

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.relational.core.sql;
import org.springframework.util.Assert;
/**
* Abstract implementation to support {@link Segment} implementations.
*
@@ -22,6 +24,24 @@ package org.springframework.data.relational.core.sql;
*/
abstract class AbstractSegment implements Segment {
private final Segment[] children;
protected AbstractSegment(Segment ... children) {
this.children = children;
}
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
for (Segment child : children) {
child.visit(visitor);
}
visitor.leave(this);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()

View File

@@ -16,35 +16,30 @@
package org.springframework.data.relational.core.sql;
/**
* {@code TOP} clause for {@code SELECT TOP }.
* An expression with an alias.
*
* @author Mark Paluch
* @author Jens Schauder
*/
public class SelectTop extends AbstractSegment implements Segment {
public class AliasedExpression extends AbstractSegment implements Aliased, Expression {
private final int count;
private final Expression expression;
private final String alias;
private SelectTop(int count) {
this.count = count;
public AliasedExpression(Expression expression, String alias) {
super(expression);
this.expression = expression;
this.alias = alias;
}
public static SelectTop create(int count) {
return new SelectTop(count);
@Override
public String getAlias() {
return alias;
}
/**
* @return the count.
*/
public int getCount() {
return count;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "TOP " + count;
return expression.toString() + " AS " + alias;
}
}

View File

@@ -15,59 +15,16 @@
*/
package org.springframework.data.relational.core.sql;
import org.springframework.util.Assert;
/**
* {@link Condition} representing an {@code AND} relation between two {@link Condition}s.
*
* @author Mark Paluch
* @see Condition#and(Condition)
*/
public class AndCondition implements Condition {
public class AndCondition extends MultipleCondition {
private final Condition left;
private final Condition right;
AndCondition(Condition left, Condition right) {
this.left = left;
this.right = right;
AndCondition(Condition... conditions) {
super(" AND ", conditions);
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
*/
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
left.visit(visitor);
right.visit(visitor);
visitor.leave(this);
}
/**
* @return the left {@link Condition}.
*/
public Condition getLeft() {
return left;
}
/**
* @return the right {@link Condition}.
*/
public Condition getRight() {
return right;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return left.toString() + " AND " + right.toString();
}
}

View File

@@ -30,6 +30,7 @@ public class AsteriskFromTable extends AbstractSegment implements Expression {
private final Table table;
AsteriskFromTable(Table table) {
super(table);
this.table = table;
}
@@ -37,16 +38,6 @@ public class AsteriskFromTable extends AbstractSegment implements Expression {
return new AsteriskFromTable(table);
}
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
table.visit(visitor);
visitor.leave(this);
}
/**
* @return the associated {@link Table}.
*/

View File

@@ -22,7 +22,7 @@ import org.springframework.lang.Nullable;
*
* @author Mark Paluch
*/
public class BindMarker implements Segment {
public class BindMarker extends AbstractSegment implements Expression {
/*
* (non-Javadoc)

View File

@@ -28,44 +28,17 @@ import org.springframework.util.Assert;
public class Column extends AbstractSegment implements Expression, Named {
private final String name;
private final @Nullable Table table;
private final Table table;
Column(String name, @Nullable Table table) {
Column(String name, Table table) {
super(table);
Assert.notNull(name, "Name must not be null");
this.name = name;
this.table = table;
}
/**
* Creates a new {@link Column}.
*
* @param name column name, must not {@literal null} or empty.
* @return the new {@link Column}.
*/
public static Column create(String name) {
Assert.hasText(name, "Name must not be null or empty");
return new Column(name, null);
}
/**
* Creates a new aliased {@link Column}.
*
* @param name column name, must not {@literal null} or empty.
* @param alias alias name, must not {@literal null} or empty.
* @return the new {@link Column}.
*/
public static Column aliased(String name, String alias) {
Assert.hasText(name, "Name must not be null or empty");
Assert.hasText(alias, "Alias must not be null or empty");
return new AliasedColumn(name, null, alias);
}
/**
* Creates a new {@link Column} associated with a {@link Table}.
*
@@ -124,23 +97,6 @@ public class Column extends AbstractSegment implements Expression, Named {
return new Column(name, table);
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
*/
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
if (table != null) {
table.visit(visitor);
}
visitor.leave(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Named#getName()
@@ -192,7 +148,7 @@ public class Column extends AbstractSegment implements Expression, Named {
private final String alias;
private AliasedColumn(String name, @Nullable Table table, String alias) {
private AliasedColumn(String name, Table table, String alias) {
super(name, table);
this.alias = alias;
}

View File

@@ -43,12 +43,7 @@ public interface Condition extends Segment {
return new OrCondition(this, other);
}
/**
* Encapsulate this {@link Condition} in a group of parentheses.
*
* @return the grouped {@link Condition}.
*/
default Condition group() {
return new ConditionGroup(this);
default Condition not() {
return new Not(this);
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* 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.springframework.data.relational.core.sql;
import org.springframework.util.Assert;
/**
* Grouped {@link Condition} wrapping one or more {@link Condition}s into a parentheses group.
*
* @author Mark Paluch
* @see Condition#group()
*/
public class ConditionGroup implements Condition {
private final Condition nested;
ConditionGroup(Condition nested) {
this.nested = nested;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
*/
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
nested.visit(visitor);
visitor.leave(this);
}
/**
* @return the nested (grouped) {@link Condition}.
*/
public Condition getNested() {
return nested;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "(" + nested.toString() + ")";
}
}

View File

@@ -46,6 +46,18 @@ public abstract class Conditions {
private Conditions() {
}
public static Condition isNull(Expression expression) {
return new IsNull(expression);
}
public static Condition isEqual(Column bar, Expression param) {
return new Equals(bar, param);
}
public static Condition in(Column bar, Expression subselectExpression) {
return new In(bar, subselectExpression);
}
static class ConstantCondition extends AbstractSegment implements Condition {
private final String condition;

View File

@@ -29,7 +29,7 @@ import org.springframework.util.Assert;
*/
class DefaultSelect implements Select {
private final @Nullable SelectTop top;
private final boolean distinct;
private final List<Expression> selectList;
private final From from;
private final long limit;
@@ -38,10 +38,10 @@ class DefaultSelect implements Select {
private final @Nullable Where where;
private final List<OrderByField> orderBy;
DefaultSelect(@Nullable SelectTop top, List<Expression> selectList, List<Table> from, long limit, long offset,
DefaultSelect(boolean distinct, List<Expression> selectList, List<Table> from, long limit, long offset,
List<Join> joins, @Nullable Condition where, List<OrderByField> orderBy) {
this.top = top;
this.distinct = distinct;
this.selectList = new ArrayList<>(selectList);
this.from = new From(from);
this.limit = limit;
@@ -69,6 +69,11 @@ class DefaultSelect implements Select {
return offset == -1 ? OptionalLong.empty() : OptionalLong.of(offset);
}
@Override
public boolean isDistinct() {
return distinct;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
@@ -80,8 +85,6 @@ class DefaultSelect implements Select {
visitor.enter(this);
visitIfNotNull(top, visitor);
selectList.forEach(it -> it.visit(visitor));
from.visit(visitor);
joins.forEach(it -> it.visit(visitor));
@@ -94,6 +97,7 @@ class DefaultSelect implements Select {
}
private void visitIfNotNull(@Nullable Visitable visitable, Visitor visitor) {
if (visitable != null) {
visitable.visit(visitor);
}

View File

@@ -32,7 +32,7 @@ import org.springframework.data.relational.core.sql.SelectBuilder.SelectWhereAnd
*/
class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAndJoin, SelectWhereAndOr {
private SelectTop top;
private boolean distinct = false;
private List<Expression> selectList = new ArrayList<>();
private List<Table> from = new ArrayList<>();
private long limit = -1;
@@ -48,19 +48,10 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
@Override
public SelectBuilder top(int count) {
top = SelectTop.create(count);
limit = count;
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder#select(java.lang.String)
*/
@Override
public DefaultSelectBuilder select(String sql) {
return select(Column.create(sql));
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder#select(org.springframework.data.relational.core.sql.Expression)
@@ -91,6 +82,12 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
return this;
}
@Override
public DefaultSelectBuilder distinct() {
distinct = true;
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFrom#from(java.lang.String)
@@ -161,28 +158,6 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndOrderBy#orderBy(java.lang.String)
*/
@Override
public DefaultSelectBuilder orderBy(String field) {
return orderBy(OrderByField.create(field));
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndOrderBy#orderBy(int[])
*/
@Override
public DefaultSelectBuilder orderBy(int... indexes) {
for (int index : indexes) {
this.orderBy.add(OrderByField.index(index));
}
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndOrderBy#orderBy(org.springframework.data.relational.core.sql.OrderByField[])
@@ -284,7 +259,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
*/
@Override
public Select build() {
DefaultSelect select = new DefaultSelect(top, selectList, from, limit, offset, joins, where, orderBy);
DefaultSelect select = new DefaultSelect(distinct, selectList, from, limit, offset, joins, where, orderBy);
SelectValidator.validate(select);
return select;
}
@@ -305,15 +280,6 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
this.selectBuilder = selectBuilder;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOn#on(java.lang.String)
*/
@Override
public SelectOnConditionComparison on(String column) {
return on(Column.create(column));
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOn#on(org.springframework.data.relational.core.sql.Expression)
@@ -325,15 +291,6 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOnConditionComparison#equals(java.lang.String)
*/
@Override
public JoinBuilder equals(String column) {
return equals(Column.create(column));
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOnConditionComparison#equals(org.springframework.data.relational.core.sql.Expression)
@@ -344,15 +301,6 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOnCondition#and(java.lang.String)
*/
@Override
public SelectOnConditionComparison and(String column) {
return and(Column.create(column));
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOnCondition#and(org.springframework.data.relational.core.sql.Expression)
@@ -381,27 +329,6 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
return new Join(JoinType.JOIN, table, condition);
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOrdered#orderBy(java.lang.String)
*/
@Override
public SelectOrdered orderBy(String field) {
selectBuilder.join(finishJoin());
return selectBuilder.orderBy(field);
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOrdered#orderBy(int[])
*/
@Override
public SelectOrdered orderBy(int... indexes) {
selectBuilder.join(finishJoin());
return selectBuilder.orderBy(indexes);
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectOrdered#orderBy(org.springframework.data.relational.core.sql.OrderByField[])

View File

@@ -30,6 +30,9 @@ public class Equals extends AbstractSegment implements Condition {
private final Expression right;
Equals(Expression left, Expression right) {
super(left, right);
this.left = left;
this.right = right;
}
@@ -49,17 +52,6 @@ public class Equals extends AbstractSegment implements Condition {
return new Equals(left, right);
}
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
left.visit(visitor);
right.visit(visitor);
visitor.leave(this);
}
/**
* @return the left {@link Expression}.
*/

View File

@@ -25,7 +25,7 @@ package org.springframework.data.relational.core.sql;
*/
public abstract class Expressions {
private static Expression ASTERISK = new ConstantExpression("*");
private static Expression ASTERISK = new SimpleExpression("*");
/**
* @return a new asterisk {@code *} expression.
@@ -41,7 +41,7 @@ public abstract class Expressions {
* @return a SQL {@link Expression}.
*/
public static Expression just(String sql) {
return new ConstantExpression(sql);
return new SimpleExpression(sql);
}
/**
@@ -55,11 +55,11 @@ public abstract class Expressions {
private Expressions() {
}
static class ConstantExpression extends AbstractSegment implements Expression {
static class SimpleExpression extends AbstractSegment implements Expression {
private final String expression;
ConstantExpression(String expression) {
SimpleExpression(String expression) {
this.expression = expression;
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.relational.core.sql;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.util.Assert;
@@ -26,7 +27,7 @@ import org.springframework.util.StringUtils;
*
* @author Mark Paluch
*/
public class From extends AbstractSegment implements Segment {
public class From extends AbstractSegment {
private final List<Table> tables;
@@ -35,25 +36,12 @@ public class From extends AbstractSegment implements Segment {
}
From(List<Table> tables) {
super(tables.toArray(new Table[]{}));
this.tables = tables;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
*/
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
tables.forEach(it -> it.visit(visitor));
visitor.leave(this);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()

View File

@@ -18,7 +18,6 @@ package org.springframework.data.relational.core.sql;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.springframework.util.Assert;
@@ -32,60 +31,6 @@ import org.springframework.util.Assert;
*/
public class Functions {
/**
* Creates a new {@link Distinct} function.
*
* @param columnNames column names to apply distinction, must not be {@literal null}.
* @return the new {@link Distinct} for {@code columns}.
*/
public static Distinct distinct(String... columnNames) {
Assert.notNull(columnNames, "Columns must not be null!");
List<Column> columns = new ArrayList<>();
for (String columnName : columnNames) {
columns.add(Column.create(columnName));
}
return distinct(columns);
}
/**
* Creates a new {@link Distinct} function.
*
* @param columns columns to apply distinction, must not be {@literal null}.
* @return the new {@link Distinct} for {@code columns}.
*/
public static Distinct distinct(Column... columns) {
Assert.notNull(columns, "Columns must not be null!");
return new Distinct(Arrays.asList(columns));
}
/**
* Creates a new {@link Distinct} function.
*
* @param columns columns to apply distinction, must not be {@literal null}.
* @return the new {@link Distinct} for {@code columns}.
*/
public static Distinct distinct(Collection<? extends Column> columns) {
Assert.notNull(columns, "Columns must not be null!");
return new Distinct(new ArrayList<>(columns));
}
/**
* Creates a new {@code COUNT} function for a single {@code column}.
*
* @param column column to apply count, must not be {@literal null} or empty.
* @return the new {@link SimpleFunction count function} for {@code column}.
*/
public static SimpleFunction count(String column) {
return count(Column.create(column));
}
/**
* Creates a new {@code COUNT} function.
*
@@ -114,6 +59,5 @@ public class Functions {
}
// Utility constructor.
private Functions() {
}
private Functions() {}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* 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.springframework.data.relational.core.sql;
/**
* @author Jens Schauder
*/
public class In extends AbstractSegment implements Condition {
private final Expression left;
private final Expression right;
public In(Expression left, Expression right) {
super(left, right);
this.left = left;
this.right = right;
}
@Override
public String toString() {
return left + " IN " + right;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* 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.springframework.data.relational.core.sql;
/**
* @author Jens Schauder
*/
public class IsNull extends AbstractSegment implements Condition {
private final Expression expression;
private final boolean negated;
public IsNull(Expression expression, boolean negated) {
super(expression);
this.expression = expression;
this.negated = negated;
}
public IsNull(Expression expression) {
this(expression, false);
}
@Override
public Condition not() {
return new IsNull(expression, !negated);
}
@Override
public String toString() {
return expression + (negated ? " IS NOT NULL" : " IS NULL");
}
public boolean isNegated() {
return negated;
}
}

View File

@@ -24,33 +24,21 @@ package org.springframework.data.relational.core.sql;
*
* @author Mark Paluch
*/
public class Join implements Segment {
public class Join extends AbstractSegment {
private final JoinType type;
private final Table joinTable;
private final Condition on;
Join(JoinType type, Table joinTable, Condition on) {
super(joinTable, on);
this.joinTable = joinTable;
this.type = type;
this.on = on;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
*/
@Override
public void visit(Visitor visitor) {
visitor.enter(this);
joinTable.visit(visitor);
on.visit(visitor);
visitor.leave(this);
}
/**
* @return join type.
*/

View File

@@ -15,38 +15,26 @@
*/
package org.springframework.data.relational.core.sql;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@code DISTINCT} {@link Expression}.
* <p/>
* * Renders to: {@code DISTINCT column1, column2, columnN}.
*
* @author Mark Paluch
* @author Jens Schauder
*/
public class Distinct extends AbstractSegment implements Expression {
public abstract class MultipleCondition extends AbstractSegment implements Condition {
private List<Column> columns;
private final List<Condition> conditions;
private final String delimiter;
Distinct(List<Column> columns) {
this.columns = columns;
}
MultipleCondition(String delimiter, Condition... conditions) {
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
*/
@Override
public void visit(Visitor visitor) {
super(conditions);
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
columns.forEach(it -> it.visit(visitor));
visitor.leave(this);
this.delimiter = delimiter;
this.conditions = Arrays.asList(conditions);
}
/*
@@ -55,6 +43,9 @@ public class Distinct extends AbstractSegment implements Expression {
*/
@Override
public String toString() {
return "DISTINCT " + StringUtils.collectionToDelimitedString(columns, ", ");
StringJoiner joiner = new StringJoiner(delimiter);
conditions.forEach(c -> joiner.add(c.toString()));
return joiner.toString();
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* 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.springframework.data.relational.core.sql;
/**
* @author Jens Schauder
*/
public class Not extends AbstractSegment implements Condition {
private final Condition condition;
public Not(Condition condition) {
super(condition);
this.condition = condition;
}
@Override
public Condition not() {
return condition;
}
@Override
public String toString() {
return "NOT " + condition.toString();
}
}

View File

@@ -15,59 +15,15 @@
*/
package org.springframework.data.relational.core.sql;
import org.springframework.util.Assert;
/**
* {@link Condition} representing an {@code OR} relation between two {@link Condition}s.
*
* @author Mark Paluch
* @see Condition#or(Condition)
*/
public class OrCondition implements Condition {
public class OrCondition extends MultipleCondition {
private final Condition left;
private final Condition right;
OrCondition(Condition left, Condition right) {
this.left = left;
this.right = right;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
*/
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
left.visit(visitor);
right.visit(visitor);
visitor.leave(this);
}
/**
* @return the left {@link Condition}.
*/
public Condition getLeft() {
return left;
}
/**
* @return the right {@link Condition}.
*/
public Condition getRight() {
return right;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return left.toString() + " OR " + right.toString();
OrCondition(Condition... conditions) {
super(" OR ", conditions);
}
}

View File

@@ -24,7 +24,7 @@ import org.springframework.util.Assert;
/**
* @author Mark Paluch
*/
public class OrderByField extends AbstractSegment implements Segment {
public class OrderByField extends AbstractSegment {
private final Expression expression;
private final @Nullable Sort.Direction direction;
@@ -32,6 +32,7 @@ public class OrderByField extends AbstractSegment implements Segment {
OrderByField(Expression expression, Direction direction, NullHandling nullHandling) {
super(expression);
Assert.notNull(expression, "Order by expression must not be null");
Assert.notNull(nullHandling, "NullHandling by expression must not be null");
@@ -44,14 +45,6 @@ public class OrderByField extends AbstractSegment implements Segment {
return new OrderByField(column, null, NullHandling.NATIVE);
}
public static OrderByField create(String name) {
return new OrderByField(Column.create(name), null, NullHandling.NATIVE);
}
public static OrderByField index(int index) {
return new OrderByField(new IndexedOrderByField(index), null, NullHandling.NATIVE);
}
public OrderByField asc() {
return new OrderByField(expression, Direction.ASC, NullHandling.NATIVE);
}
@@ -64,18 +57,6 @@ public class OrderByField extends AbstractSegment implements Segment {
return new OrderByField(expression, direction, nullHandling);
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
*/
@Override
public void visit(Visitor visitor) {
visitor.enter(this);
expression.visit(visitor);
visitor.leave(this);
}
public Expression getExpression() {
return expression;
}
@@ -97,11 +78,4 @@ public class OrderByField extends AbstractSegment implements Segment {
public String toString() {
return direction != null ? expression.toString() + " " + direction : expression.toString();
}
static class IndexedOrderByField extends Column implements Expression {
IndexedOrderByField(int index) {
super("" + index, null);
}
}
}

View File

@@ -63,26 +63,6 @@ public abstract class SQL {
return Select.builder();
}
/**
* Creates a new {@link SelectTop SELECT TOP count} segment.
*
* @param count the TOP count.
* @return the new {@link SelectTop} segment.
*/
public static SelectTop top(int count) {
return SelectTop.create(count);
}
/**
* Creates a new {@link Column}.
*
* @param name column name, must not be {@literal null} or empty.
* @return the column with {@code name}.
*/
public static Column column(String name) {
return Column.create(name);
}
/**
* Creates a new {@link Column} associated with a source {@link Table}.
*

View File

@@ -22,7 +22,6 @@ import java.util.OptionalLong;
* Visiting order:
* <ol>
* <li>Self</li>
* <li>{@link SelectTop top clause}</li>
* <li>{@link Column SELECT columns} </li>
* <li>{@link Table FROM tables} clause</li>
* <li>{@link Join JOINs}</li>
@@ -34,7 +33,7 @@ import java.util.OptionalLong;
* @see SelectBuilder
* @see SQL
*/
public interface Select extends Visitable {
public interface Select extends Segment, Visitable {
/**
@@ -59,4 +58,11 @@ public interface Select extends Visitable {
* @return
*/
OptionalLong getOffset();
/**
* Flag if this select is to return distinct rows.
*
* @return
*/
boolean isDistinct();
}

View File

@@ -29,25 +29,14 @@ public interface SelectBuilder {
*
* @param count the top count.
* @return {@code this} {@link SelectBuilder}.
* @see SelectTop
*/
SelectBuilder top(int count);
/**
* Include an arbitrary {@code sql} select list item. The {@code sql} is encapsulated into a simple {@link Expression}.
*
* @param sql the select list item.
* @return {@code this} builder.
* @see SQL#column(String)
*/
SelectAndFrom select(String sql);
/**
* Include a {@link Expression} in the select list.
*
* @param expression the expression to include.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#column(String)
*/
SelectAndFrom select(Expression expression);
@@ -57,7 +46,6 @@ public interface SelectBuilder {
*
* @param expressions the expressions to include.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#columns(String...)
*/
SelectAndFrom select(Expression... expressions);
@@ -67,31 +55,27 @@ public interface SelectBuilder {
*
* @param expressions the expressions to include.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#columns(String...)
*/
SelectAndFrom select(Collection<? extends Expression> expressions);
/**
* Makes the select statement distinct
*
* @return {@code this} builder.
*/
SelectAndFrom distinct();
/**
* Builder exposing {@code select} and {@code from} methods.
*/
interface SelectAndFrom extends SelectFrom {
/**
* Include an arbitrary {@code sql} select list item. The {@code sql} is encapsulated into a simple {@link Expression}. Multiple calls to this or other {@code select} methods keep adding items to the select list and do not replace previously contained items.
*
* @param sql the select list item.
* @return {@code this} builder.
* @see SQL#column(String)
*/
SelectFrom select(String sql);
/**
* Include a {@link Expression} in the select list. Multiple calls to this or other {@code select} methods keep adding items to the select list and do not replace previously contained items.
*
* @param expression the expression to include.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#column(String)
*/
SelectFrom select(Expression expression);
@@ -101,7 +85,6 @@ public interface SelectBuilder {
*
* @param expressions the expressions to include.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#columns(String...)
*/
SelectFrom select(Expression... expressions);
@@ -111,11 +94,17 @@ public interface SelectBuilder {
*
* @param expressions the expressions to include.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#columns(String...)
*/
SelectFrom select(Collection<? extends Expression> expressions);
/**
* Makes the select statement distinct
*
* @return {@code this} builder.
*/
SelectAndFrom distinct();
/**
* Declare a {@link Table} to {@code SELECT … FROM}.
* Multiple calls to this or other {@code from} methods keep adding items to the select list and do not replace previously contained items.
@@ -229,15 +218,9 @@ public interface SelectBuilder {
@Override
SelectFromAndOrderBy from(Collection<? extends Table> tables);
@Override
SelectFromAndOrderBy orderBy(String field);
@Override
SelectFromAndOrderBy orderBy(Column... columns);
@Override
SelectFromAndOrderBy orderBy(int... indexes);
@Override
SelectFromAndOrderBy orderBy(OrderByField... orderByFields);
@@ -379,39 +362,19 @@ public interface SelectBuilder {
*/
interface SelectOrdered extends BuildSelect {
/**
* Add an order by {@code field} using default sort semantics.
*
* @param field field name, must not be {@literal null} or empty.
* @return {@code this} builder.
* @see OrderByField#create(String)
*/
SelectOrdered orderBy(String field);
/**
* Add one or more {@link Column columns} to order by.
*
* @param columns the columns to order by.
* @return {@code this} builder.
* @see OrderByField#create(String)
*/
SelectOrdered orderBy(Column... columns);
/**
* Add an order by field using {@code indexes} using default sort semantics.
*
* @param indexes field indexes as declared in the select list.
* @return {@code this} builder.
* @see OrderByField#index(int)
*/
SelectOrdered orderBy(int... indexes);
/**
* Add one or more {@link OrderByField order by fields}.
*
* @param orderByFields the fields to order by.
* @return {@code this} builder.
* @see OrderByField#create(String)
*/
SelectOrdered orderBy(OrderByField... orderByFields);
@@ -420,7 +383,6 @@ public interface SelectBuilder {
*
* @param orderByFields the fields to order by.
* @return {@code this} builder.
* @see OrderByField#create(String)
*/
SelectOrdered orderBy(Collection<? extends OrderByField> orderByFields);
}
@@ -496,22 +458,12 @@ public interface SelectBuilder {
*/
interface SelectOn {
/**
* Declare the source column in the {@code JOIN}.
*
* @param column name of the source column, must not be {@literal null} or empty.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#column(String)
*/
SelectOnConditionComparison on(String column);
/**
* Declare the source column in the {@code JOIN}.
*
* @param column the source column, must not be {@literal null} or empty.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#column(String)
*/
SelectOnConditionComparison on(Expression column);
@@ -522,22 +474,11 @@ public interface SelectBuilder {
*/
interface SelectOnConditionComparison {
/**
* Declare an equals {@link Condition} between the source column and the target {@code column}.
*
* @param column name of the target column, must not be {@literal null} or empty.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#column(String)
*/
SelectFromAndJoinCondition equals(String column);
/**
* Declare an equals {@link Condition} between the source column and the target {@link Column}.
*
* @param column the target column, must not be {@literal null}.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#column(String)
*/
SelectFromAndJoinCondition equals(Expression column);
@@ -548,22 +489,11 @@ public interface SelectBuilder {
*/
interface SelectOnCondition extends SelectJoin, BuildSelect {
/**
* Declare an additional source column in the {@code JOIN}.
*
* @param column the column name, must not be {@literal null} or empty.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#column(String)
*/
SelectOnConditionComparison and(String column);
/**
* Declare an additional source column in the {@code JOIN}.
*
* @param column the column, must not be {@literal null}.
* @return {@code this} builder.
* @see SQL#column(String)
* @see Table#column(String)
*/
SelectOnConditionComparison and(Expression column);

View File

@@ -82,7 +82,7 @@ class SelectValidator implements Visitor {
}
if (segment instanceof Column
&& (parent instanceof Select || parent instanceof SimpleFunction || parent instanceof Distinct)) {
&& (parent instanceof Select || parent instanceof SimpleFunction)) {
selectFieldCount++;
Table table = ((Column) segment).getTable();
@@ -120,8 +120,7 @@ class SelectValidator implements Visitor {
}
if (segment instanceof Join || segment instanceof OrderByField || segment instanceof From
|| segment instanceof Select || segment instanceof Where || segment instanceof SimpleFunction
|| segment instanceof Distinct) {
|| segment instanceof Select || segment instanceof Where || segment instanceof SimpleFunction) {
parent = segment;
}
}
@@ -131,6 +130,5 @@ class SelectValidator implements Visitor {
* @see org.springframework.data.relational.core.sql.Visitor#leave(org.springframework.data.relational.core.sql.Visitable)
*/
@Override
public void leave(Visitable segment) {
}
public void leave(Visitable segment) {}
}

View File

@@ -31,6 +31,9 @@ public class SimpleCondition extends AbstractSegment implements Condition {
private final String predicate;
SimpleCondition(Expression expression, String comparator, String predicate) {
super(expression);
this.expression = expression;
this.comparator = comparator;
this.predicate = predicate;
@@ -56,20 +59,6 @@ public class SimpleCondition extends AbstractSegment implements Condition {
return predicate;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
*/
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
expression.visit(visitor);
visitor.leave(this);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()

View File

@@ -31,6 +31,9 @@ public class SimpleFunction extends AbstractSegment implements Expression {
private List<Expression> expressions;
SimpleFunction(String functionName, List<Expression> expressions) {
super(expressions.toArray(new Expression[]{}));
this.functionName = functionName;
this.expressions = expressions;
}
@@ -48,20 +51,6 @@ public class SimpleFunction extends AbstractSegment implements Expression {
return new AliasedFunction(functionName, expressions, alias);
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitable#visit(org.springframework.data.relational.core.sql.Visitor)
*/
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
expressions.forEach(it -> it.visit(visitor));
visitor.leave(this);
}
/**
* @return the function name.
*/

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* 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.springframework.data.relational.core.sql;
/**
* @author Jens Schauder
*/
public class SubselectExpression extends AbstractSegment implements Expression {
private final Select subselect;
public SubselectExpression(Select subselect) {
super(subselect);
this.subselect = subselect;
}
@Override
public String toString() {
return "(" + subselect.toString() + ")";
}
}

View File

@@ -28,11 +28,12 @@ import org.springframework.util.Assert;
*
* @author Mark Paluch
*/
public class Table extends AbstractSegment implements Segment, Named {
public class Table extends AbstractSegment {
private final String name;
Table(String name) {
super();
this.name = name;
}

View File

@@ -22,26 +22,17 @@ import org.springframework.util.Assert;
*
* @author Mark Paluch
*/
public class Where extends AbstractSegment implements Segment {
public class Where extends AbstractSegment {
private final Condition condition;
Where(Condition condition) {
super(condition);
this.condition = condition;
}
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
visitor.enter(this);
condition.visit(visitor);
visitor.leave(this);
}
@Override
public String toString() {
return "WHERE " + condition.toString();

View File

@@ -17,14 +17,15 @@ package org.springframework.data.relational.core.sql;
import java.util.OptionalLong;
import java.util.Stack;
import java.util.function.Consumer;
import org.springframework.util.Assert;
/**
* Naive SQL renderer that does not consider dialect specifics. This class is to evaluate requirements of a SQL renderer.
* Naive SQL renderer that does not consider dialect specifics. This class is to evaluate requirements of a SQL
* renderer.
*
* @author Mark Paluch
* @author Jens Schauder
*/
public class NaiveSqlRenderer {
@@ -64,286 +65,591 @@ public class NaiveSqlRenderer {
*/
public String render() {
RenderVisitor visitor = new RenderVisitor();
StackBasedVisitor visitor = new StackBasedVisitor();
select.visit(visitor);
return visitor.builder.toString();
return visitor.selectStatementVisitor.getValue();
}
/**
* {@link Visitor} to render the SQL.
*/
static class RenderVisitor implements Visitor {
interface ValuedVisitor extends Visitor {
String getValue();
}
StringBuilder builder = new StringBuilder();
static class StackBasedVisitor implements Visitor {
private boolean hasDistinct = false;
private boolean hasOrderBy = false;
private boolean nextRequiresComma = false;
private boolean nextExpressionRequiresContinue = false;
private boolean nextConditionRequiresContinue = false;
private boolean inSelectList = false;
private Stack<Visitable> segments = new Stack<>();
private Stack<Visitor> visitors = new Stack<>();
private SelectStatementVisitor selectStatementVisitor = new SelectStatementVisitor();
{
visitors.push(segment -> {});
visitors.push(selectStatementVisitor);
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitor#enter(org.springframework.data.relational.core.sql.Visitable)
*/
@Override
public void enter(Visitable segment) {
if (segment instanceof Select) {
builder.append("SELECT ");
inSelectList = true;
}
if (segment instanceof From) {
nextRequiresComma = false;
addSpaceIfNecessary();
builder.append("FROM ");
}
if (segment instanceof From || segment instanceof Join) {
inSelectList = false;
}
if (segment instanceof Distinct && !hasDistinct) {
builder.append("DISTINCT ");
hasDistinct = true;
}
if (segment instanceof OrderByField && !hasOrderBy) {
addSpaceIfNecessary();
builder.append("ORDER BY ");
nextRequiresComma = false;
hasOrderBy = true;
}
if (segment instanceof SimpleFunction) {
nextRequiresComma = false;
builder.append(((SimpleFunction) segment).getFunctionName()).append("(");
}
if (segment instanceof Table && segments.peek() instanceof From) {
addCommaIfNecessary();
Table table = (Table) segment;
builder.append(table.getName());
ifAliased(segment, aliased -> builder.append(" AS ").append(aliased.getAlias()));
nextRequiresComma = true;
}
if (segment instanceof Join) {
addSpaceIfNecessary();
Join join = (Join) segment;
builder.append(join.getType());
}
if (segment instanceof Table && segments.peek() instanceof Join) {
addSpaceIfNecessary();
Table table = (Table) segment;
builder.append(table.getName());
ifAliased(segment, aliased -> builder.append(" AS ").append(aliased.getAlias()));
}
if (segment instanceof Condition) {
nextRequiresComma = false;
}
if (segment instanceof Condition && segments.peek() instanceof Join) {
addSpaceIfNecessary();
builder.append("ON ");
}
if ((segment instanceof Expression || segment instanceof Condition) && segments.peek() instanceof Condition) {
if (segment instanceof Expression) {
if (!nextExpressionRequiresContinue) {
nextExpressionRequiresContinue = true;
} else {
renderCombinator((Condition) segments.peek());
nextExpressionRequiresContinue = false;
}
}
}
if (segment instanceof ConditionGroup) {
addSpaceIfNecessary();
builder.append("(");
} else if (segment instanceof Condition && segments.peek() instanceof Condition) {
if (!nextConditionRequiresContinue) {
nextConditionRequiresContinue = true;
} else {
renderCombinator((Condition) segments.peek());
nextConditionRequiresContinue = false;
}
}
segments.add(segment);
Visitor delegate = visitors.peek();
delegate.enter(segment);
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Visitor#leave(org.springframework.data.relational.core.sql.Visitable)
*/
@Override
public void leave(Visitable segment) {
segments.pop();
Visitable parent = segments.isEmpty() ? null : segments.peek();
Visitor delegate = visitors.peek();
delegate.leave(segment);
}
if (segment instanceof Condition) {
nextExpressionRequiresContinue = false;
}
/**
* Handles a sequence of {@link Visitable} until encountering the first that does not matches the expectations. When
* a not matching element is encountered it pops itself from the stack and delegates the call to the now top most
* element of the stack.
*/
abstract class ReadWhileMatchesVisitor implements Visitor {
if (segment instanceof Select) {
private Visitable currentSegment = null;
private Visitor nextVisitor;
// Postgres syntax
Select select = (Select) segment;
abstract boolean matches(Visitable segment);
OptionalLong limit = select.getLimit();
OptionalLong offset = select.getOffset();
void enterMatched(Visitable segment) {}
limit.ifPresent(count -> {
addSpaceIfNecessary();
builder.append("LIMIT ").append(count);
});
void enterSub(Visitable segment) {}
offset.ifPresent(count -> {
addSpaceIfNecessary();
builder.append("OFFSET ").append(count);
});
}
void leaveMatched(Visitable segment) {}
if (segment instanceof Table && parent instanceof Column) {
void leaveSub(Visitable segment) {}
if (inSelectList || !(parent instanceof Aliased)) {
@Override
public void enter(Visitable segment) {
if (nextRequiresComma) {
builder.append(", ");
nextRequiresComma = false;
if (currentSegment == null) {
if (matches(segment)) {
currentSegment = segment;
enterMatched(segment);
} else {
Visitor popped = visitors.pop();
Assert.isTrue(popped == this, "Popped the wrong visitor from the stack!");
nextVisitor = visitors.peek();
nextVisitor.enter(segment);
}
} else {
enterSub(segment);
}
}
@Override
public void leave(Visitable segment) {
if (currentSegment == null) {
// we are receiving the leave event of the element above
visitors.pop();
nextVisitor = visitors.peek();
nextVisitor.leave(segment);
} else if (segment == currentSegment) {
currentSegment = null;
leaveMatched(segment);
} else {
leaveSub(segment);
}
}
}
/**
* Visits exactly one element that must match the expectations as defined in {@link #matches(Visitable)}. Ones
* handled it pops itself from the stack.
*/
abstract class ReadOneVisitor implements Visitor {
private Visitable currentSegment;
abstract boolean matches(Visitable segment);
void enterMatched(Visitable segment) {}
void enterSub(Visitable segment) {}
void leaveMatched(Visitable segment) {}
void leaveSub(Visitable segment) {}
@Override
public void enter(Visitable segment) {
if (currentSegment == null) {
if (matches(segment)) {
currentSegment = segment;
enterMatched(segment);
} else {
Assert.isTrue(visitors.pop() == this, "Popped wrong visitor instance.");
visitors.peek().enter(segment);
}
} else {
enterSub(segment);
}
}
@Override
public void leave(Visitable segment) {
if (currentSegment == null) {
Assert.isTrue(visitors.pop() == this, "Popped wrong visitor instance.");
visitors.peek().leave(segment);
} else if (segment == currentSegment) {
leaveMatched(segment);
Assert.isTrue(visitors.pop() == this, "Popped wrong visitor instance.");
} else {
leaveSub(segment);
}
}
}
class SelectStatementVisitor extends ReadOneVisitor implements ValuedVisitor {
private StringBuilder builder = new StringBuilder();
private SelectListVisitor selectListVisitor = new SelectListVisitor();
private FromClauseVisitor fromClauseVisitor = new FromClauseVisitor();
private JoinVisitor joinVisitor = new JoinVisitor();
private WhereClauseVisitor whereClauseVisitor = new WhereClauseVisitor();
private OrderByClauseVisitor orderByClauseVisitor = new OrderByClauseVisitor();
@Override
boolean matches(Visitable segment) {
return segment instanceof Select;
}
@Override
void enterMatched(Visitable segment) {
visitors.push(orderByClauseVisitor);
visitors.push(whereClauseVisitor);
visitors.push(joinVisitor);
visitors.push(fromClauseVisitor);
visitors.push(selectListVisitor);
}
@Override
void leaveMatched(Visitable segment) {
builder.append("SELECT ");
if (((Select) segment).isDistinct()) {
builder.append("DISTINCT ");
}
builder.append(selectListVisitor.getValue()) //
.append(fromClauseVisitor.getValue()) //
.append(joinVisitor.getValue()) //
.append(whereClauseVisitor.getValue());
builder.append(orderByClauseVisitor.getValue());
OptionalLong limit = ((Select) segment).getLimit();
if (limit.isPresent()) {
builder.append(" LIMIT ").append(limit.getAsLong());
}
OptionalLong offset = ((Select) segment).getOffset();
if (offset.isPresent()) {
builder.append(" OFFSET ").append(offset.getAsLong());
}
}
@Override
public String getValue() {
return builder.toString();
}
}
class SelectListVisitor extends ReadWhileMatchesVisitor implements ValuedVisitor {
private StringBuilder builder = new StringBuilder();
private boolean first = true;
private boolean insideFunction = false; // this is hackery and should be fix with a proper visitor for
// subelements.
@Override
boolean matches(Visitable segment) {
return segment instanceof Expression;
}
@Override
void enterMatched(Visitable segment) {
if (!first) {
builder.append(", ");
}
if (segment instanceof SimpleFunction) {
builder.append(((SimpleFunction) segment).getFunctionName()).append("(");
insideFunction = true;
} else {
insideFunction = false;
}
}
@Override
void leaveMatched(Visitable segment) {
first = false;
if (segment instanceof SimpleFunction) {
builder.append(")");
} else if (segment instanceof Column) {
builder.append(((Column) segment).getName());
if (segment instanceof Column.AliasedColumn) {
builder.append(" AS ").append(((Column.AliasedColumn) segment).getAlias());
}
}
}
@Override
void leaveSub(Visitable segment) {
if (segment instanceof Table) {
builder.append(((Table) segment).getReferenceName()).append('.');
}
}
if (insideFunction) {
if (segment instanceof Column
&& (parent instanceof Select || parent instanceof Distinct || parent instanceof SimpleFunction)) {
addCommaIfNecessary();
Column column = (Column) segment;
builder.append(column.getName());
if (!(parent instanceof SimpleFunction)) {
ifAliased(segment, aliased -> builder.append(" AS ").append(aliased.getAlias()));
if (segment instanceof SimpleFunction) {
builder.append(")");
} else if (segment instanceof Column) {
builder.append(((Column) segment).getName());
if (segment instanceof Column.AliasedColumn) {
builder.append(" AS ").append(((Column.AliasedColumn) segment).getAlias());
}
first = false;
}
}
nextRequiresComma = true;
}
if (segment instanceof Column && (parent instanceof Condition || parent instanceof OrderByField)) {
@Override
public String getValue() {
return builder.toString();
}
}
addCommaIfNecessary();
private class FromClauseVisitor extends ReadOneVisitor implements ValuedVisitor {
Column column = (Column) segment;
private FromTableVisitor fromTableVisitor = new FromTableVisitor();
builder.append(column.getReferenceName());
@Override
boolean matches(Visitable segment) {
return segment instanceof From;
}
if (segment instanceof OrderByField) {
@Override
void enterMatched(Visitable segment) {
visitors.push(fromTableVisitor);
}
OrderByField orderBy = (OrderByField) segment;
@Override
public String getValue() {
return " FROM " + fromTableVisitor.getValue();
}
}
if (orderBy.getDirection() != null) {
builder.append(' ').append(orderBy.getDirection());
private class FromTableVisitor extends ReadWhileMatchesVisitor implements ValuedVisitor {
private final StringBuilder builder = new StringBuilder();
private boolean first = true;
@Override
boolean matches(Visitable segment) {
return segment instanceof Table;
}
@Override
void enterMatched(Visitable segment) {
if (!first) {
builder.append(", ");
}
first = false;
builder.append(((Table) segment).getName());
if (segment instanceof Table.AliasedTable) {
builder.append(" AS ").append(((Table.AliasedTable) segment).getAlias());
}
nextRequiresComma = true;
}
if (segment instanceof SimpleFunction) {
builder.append(")");
ifAliased(segment, aliased -> builder.append(" AS ").append(aliased.getAlias()));
nextRequiresComma = true;
}
if (segment instanceof ConditionGroup) {
builder.append(")");
}
if (segment instanceof SimpleCondition) {
nextRequiresComma = false;
SimpleCondition condition = (SimpleCondition) segment;
builder.append(' ').append(condition.getPredicate()).append(' ').append(condition.getPredicate());
@Override
public String getValue() {
return builder.toString();
}
}
private void addCommaIfNecessary() {
if (nextRequiresComma) {
builder.append(", ");
private class JoinVisitor extends ReadWhileMatchesVisitor implements ValuedVisitor {
private StringBuilder internal = new StringBuilder();
private JoinTableAndConditionVisitor subvisitor;
@Override
boolean matches(Visitable segment) {
return segment instanceof Join;
}
@Override
void enterMatched(Visitable segment) {
subvisitor = new JoinTableAndConditionVisitor();
visitors.push(subvisitor);
}
@Override
void leaveMatched(Visitable segment) {
append(" JOIN ");
append(subvisitor.getValue());
}
@Override
public String getValue() {
return internal.toString();
}
void append(String s) {
internal.append(s);
}
}
private class JoinTableAndConditionVisitor extends ReadWhileMatchesVisitor implements ValuedVisitor {
private final StringBuilder builder = new StringBuilder();
boolean inCondition = false;
@Override
boolean matches(Visitable segment) {
return segment instanceof Table || segment instanceof Condition;
}
@Override
void enterMatched(Visitable segment) {
if (segment instanceof Table && !inCondition) {
builder.append(((Table) segment).getName());
if (segment instanceof Table.AliasedTable) {
builder.append(" AS ").append(((Table.AliasedTable) segment).getAlias());
}
} else if (segment instanceof Condition && !inCondition) {
builder.append(" ON ");
builder.append(segment);
inCondition = true;
}
}
@Override
public String getValue() {
return builder.toString();
}
}
private void addSpaceIfNecessary() {
private class WhereClauseVisitor extends ReadOneVisitor implements ValuedVisitor {
if (requiresSpace()) {
builder.append(' ');
private ValuedVisitor conditionVisitor = new ConditionVisitor();
private StringBuilder internal = new StringBuilder();
@Override
boolean matches(Visitable segment) {
return segment instanceof Where;
}
@Override
void enterMatched(Visitable segment) {
internal.append(" WHERE ");
visitors.push(conditionVisitor);
}
@Override
void leaveMatched(Visitable segment) {
internal.append(conditionVisitor.getValue());
// builder.append(internal);
}
@Override
public String getValue() {
return internal.toString();
}
}
private void renderCombinator(Condition condition) {
private class ConditionVisitor extends ReadOneVisitor implements ValuedVisitor {
if (condition instanceof Equals) {
builder.append(" = ");
private StringBuilder builder = new StringBuilder();
ValuedVisitor left;
ValuedVisitor right;
@Override
boolean matches(Visitable segment) {
return segment instanceof Condition;
}
if (condition instanceof AndCondition) {
builder.append(" AND ");
@Override
void enterMatched(Visitable segment) {
if (segment instanceof MultipleCondition) {
left = new ConditionVisitor();
right = new ConditionVisitor();
visitors.push(right);
visitors.push(left);
} else if (segment instanceof IsNull) {
left = new ExpressionVisitor();
visitors.push(left);
} else if (segment instanceof Equals || segment instanceof In) {
left = new ExpressionVisitor();
right = new ExpressionVisitor();
visitors.push(right);
visitors.push(left);
}
}
if (condition instanceof OrCondition) {
builder.append(" OR ");
@Override
void leaveMatched(Visitable segment) {
if (segment instanceof AndCondition) {
builder.append(left.getValue()) //
.append(" AND ") //
.append(right.getValue());
} else if (segment instanceof OrCondition) {
builder.append("(") //
.append(left.getValue()) //
.append(" OR ") //
.append(right.getValue()) //
.append(")");
} else if (segment instanceof IsNull) {
builder.append(left.getValue());
if (((IsNull) segment).isNegated()) {
builder.append(" IS NOT NULL");
} else {
builder.append(" IS NULL");
}
} else if (segment instanceof Equals) {
builder.append(left.getValue()).append(" = ").append(right.getValue());
} else if (segment instanceof In) {
builder.append(left.getValue()).append(" IN ").append("(").append(right.getValue()).append(")");
}
}
@Override
public String getValue() {
return builder.toString();
}
}
private boolean requiresSpace() {
return builder.length() != 0 && builder.charAt(builder.length() - 1) != ' ';
}
private class ExpressionVisitor extends ReadOneVisitor implements ValuedVisitor {
private void ifAliased(Object segment, Consumer<Aliased> aliasedConsumer) {
private String value = "";
private SelectStatementVisitor valuedVisitor;
if (segment instanceof Aliased) {
aliasedConsumer.accept((Aliased) segment);
@Override
boolean matches(Visitable segment) {
return segment instanceof Expression;
}
@Override
void enterMatched(Visitable segment) {
if (segment instanceof SubselectExpression) {
valuedVisitor = new SelectStatementVisitor();
visitors.push(valuedVisitor);
} else if (segment instanceof Column) {
value = ((Column) segment).getTable().getName() + "." + ((Column) segment).getName();
} else if (segment instanceof BindMarker) {
if (segment instanceof BindMarker.NamedBindMarker) {
value = ":" + ((BindMarker.NamedBindMarker) segment).getName();
} else {
value = segment.toString();
}
}
}
@Override
void leaveMatched(Visitable segment) {
if (valuedVisitor != null) {
value = valuedVisitor.getValue();
}
}
@Override
public String getValue() {
return value;
}
}
private class OrderByClauseVisitor extends ReadWhileMatchesVisitor implements ValuedVisitor {
StringBuilder builder = new StringBuilder();
boolean first = true;
@Override
boolean matches(Visitable segment) {
return segment instanceof OrderByField;
}
@Override
void enterMatched(Visitable segment) {
if (!first) {
builder.append(", ");
} else {
builder.append(" ORDER BY ");
}
first = false;
}
@Override
void leaveMatched(Visitable segment) {
OrderByField field = (OrderByField) segment;
if (field.getDirection() != null) {
builder.append(" ") //
.append(field.getDirection());
}
}
@Override
void leaveSub(Visitable segment) {
if (segment instanceof Column) {
builder.append(((Column) segment).getReferenceName());
}
}
@Override
public String getValue() {
return builder.toString();
}
}
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.data.relational.core.sql;
import static org.assertj.core.api.Assertions.*;
import org.junit.Ignore;
import org.junit.Test;
/**
@@ -29,9 +30,12 @@ public class NaiveSqlRendererUnitTests {
@Test // DATAJDBC-309
public void shouldRenderSingleColumn() {
Select select = Select.builder().select("foo").from("bar").build();
Table bar = SQL.table("bar");
Column foo = bar.column("foo");
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo FROM bar");
Select select = Select.builder().select(foo).from(bar).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT bar.foo FROM bar");
}
@Test // DATAJDBC-309
@@ -50,7 +54,8 @@ public class NaiveSqlRendererUnitTests {
Table table1 = Table.create("table1");
Table table2 = Table.create("table2");
Select select = Select.builder().select(table1.column("col1")).select(table2.column("col2")).from(table1).from(table2).build();
Select select = Select.builder().select(table1.column("col1")).select(table2.column("col2")).from(table1)
.from(table2).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT table1.col1, table2.col2 FROM table1, table2");
}
@@ -58,17 +63,25 @@ public class NaiveSqlRendererUnitTests {
@Test // DATAJDBC-309
public void shouldRenderDistinct() {
Select select = Select.builder().select(Functions.distinct("foo", "bar")).from("bar").build();
Table table = SQL.table("bar");
Column foo = table.column("foo");
Column bar = table.column("bar");
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT DISTINCT foo, bar FROM bar");
Select select = Select.builder().distinct().select(foo, bar).from(table).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT DISTINCT bar.foo, bar.bar FROM bar");
}
@Test // DATAJDBC-309
public void shouldRenderCountFunction() {
Select select = Select.builder().select(Functions.count("foo"), Column.create("bar")).from("bar").build();
Table table = SQL.table("bar");
Column foo = table.column("foo");
Column bar = table.column("bar");
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT COUNT(foo), bar FROM bar");
Select select = Select.builder().select(Functions.count(foo), bar).from(table).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT COUNT(bar.foo), bar.bar FROM bar");
}
@Test // DATAJDBC-309
@@ -81,8 +94,8 @@ public class NaiveSqlRendererUnitTests {
.join(department).on(employee.column("department_id")).equals(department.column("id")) //
.build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee " +
"JOIN department ON employee.department_id = department.id");
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee "
+ "JOIN department ON employee.department_id = department.id");
}
@Test // DATAJDBC-309
@@ -96,9 +109,8 @@ public class NaiveSqlRendererUnitTests {
.and(employee.column("tenant")).equals(department.column("tenant")) //
.build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee " +
"JOIN department ON employee.department_id = department.id " +
"AND employee.tenant = department.tenant");
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee "
+ "JOIN department ON employee.department_id = department.id " + "AND employee.tenant = department.tenant");
}
@Test // DATAJDBC-309
@@ -114,18 +126,9 @@ public class NaiveSqlRendererUnitTests {
.join(tenant).on(tenant.column("tenant_id")).equals(department.column("tenant")) //
.build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee " +
"JOIN department ON employee.department_id = department.id " +
"AND employee.tenant = department.tenant " +
"JOIN tenant AS tenant_base ON tenant_base.tenant_id = department.tenant");
}
@Test // DATAJDBC-309
public void shouldRenderOrderByIndex() {
Select select = Select.builder().select(Functions.count("foo"), Column.create("bar")).from("bar").orderBy(1, 2).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT COUNT(foo), bar FROM bar ORDER BY 1, 2");
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT employee.id, department.name FROM employee "
+ "JOIN department ON employee.department_id = department.id " + "AND employee.tenant = department.tenant "
+ "JOIN tenant AS tenant_base ON tenant_base.tenant_id = department.tenant");
}
@Test // DATAJDBC-309
@@ -136,14 +139,96 @@ public class NaiveSqlRendererUnitTests {
Select select = Select.builder().select(column).from(employee).orderBy(OrderByField.from(column).asc()).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT emp.name AS emp_name FROM employee AS emp ORDER BY emp_name ASC");
assertThat(NaiveSqlRenderer.render(select))
.isEqualTo("SELECT emp.name AS emp_name FROM employee AS emp ORDER BY emp_name ASC");
}
@Test // DATAJDBC-309
public void shouldRenderOrderLimitOffset() {
Select select = Select.builder().select(Column.create("bar")).from("foo").limitOffset(10, 20).build();
Table table = SQL.table("foo");
Column bar = table.column("bar");
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT bar FROM foo LIMIT 10 OFFSET 20");
Select select = Select.builder().select(bar).from("foo").limitOffset(10, 20).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo LIMIT 10 OFFSET 20");
}
@Test // DATAJDBC-309
public void shouldRenderIsNull() {
Table table = SQL.table("foo");
Column bar = table.column("bar");
Select select = Select.builder().select(bar).from(table).where(Conditions.isNull(bar)).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IS NULL");
}
@Test // DATAJDBC-309
public void shouldRenderNotNull() {
Table table = SQL.table("foo");
Column bar = table.column("bar");
Select select = Select.builder().select(bar).from(table).where(Conditions.isNull(bar).not()).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IS NOT NULL");
}
@Test // DATAJDBC-309
public void shouldRenderEqualityCondition() {
Table table = SQL.table("foo");
Column bar = table.column("bar");
Select select = Select.builder().select(bar).from(table).where(Conditions.isEqual(bar, new BindMarker.NamedBindMarker("name"))).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar = :name");
}
@Test // DATAJDBC-309
public void shouldRendersAndOrConditionWithProperParentheses() {
Table table = SQL.table("foo");
Column bar = table.column("bar");
Column baz = table.column("baz");
Select select = Select.builder().select(bar).from(table).where(
Conditions.isEqual(bar, new BindMarker.NamedBindMarker("name"))
.or(Conditions.isEqual(bar, new BindMarker.NamedBindMarker("name2")))
.and(Conditions.isNull(baz))
).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE (foo.bar = :name OR foo.bar = :name2) AND foo.baz IS NULL");
}
@Test // DATAJDBC-309
public void shouldInWithNamedParameter() {
Table table = SQL.table("foo");
Column bar = table.column("bar");
Select select = Select.builder().select(bar).from(table).where(
Conditions.in(bar, new BindMarker.NamedBindMarker("name"))
).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IN (:name)");
}
@Test // DATAJDBC-309
public void shouldRenderInSubselect() {
Table foo = SQL.table("foo");
Column bar = foo.column("bar");
Table floo = SQL.table("floo");
Column bah = floo.column("bah");
Select subselect = Select.builder().select(bah).from(floo).build();
Select select = Select.builder().select(bar).from(foo).where(Conditions.in(bar, new SubselectExpression(subselect))).build();
assertThat(NaiveSqlRenderer.render(select)).isEqualTo("SELECT foo.bar FROM foo WHERE foo.bar IN (SELECT floo.bah FROM floo)");
}
}

View File

@@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalLong;
import org.junit.Test;
import org.springframework.data.relational.core.sql.Join.JoinType;
@@ -35,17 +36,17 @@ public class SelectBuilderUnitTests {
SelectBuilder builder = SQL.select();
Column foo = SQL.column("foo");
Column bar = SQL.column("bar");
Table table = SQL.table("mytable");
Column foo = table.column("foo");
Column bar = table.column("bar");
Select select = builder.select(foo, bar).from(table).build();
CapturingSelectVisitor visitor = new CapturingSelectVisitor();
select.visit(visitor);
assertThat(visitor.enter).containsSequence(foo, bar, new From(table), table);
assertThat(visitor.leave).containsSequence(foo, bar, table, new From(table));
assertThat(visitor.enter).containsSequence(foo, table, bar, table, new From(table), table);
assertThat(visitor.leave).containsSequence(table, foo, table, bar, table, new From(table));
}
@Test // DATAJDBC-309
@@ -53,15 +54,16 @@ public class SelectBuilderUnitTests {
SelectBuilder builder = SQL.select();
Column foo = SQL.column("foo");
Table table = SQL.table("mytable");
Column foo = table.column("foo");
Select select = builder.top(10).select(foo).from(table).build();
CapturingSelectVisitor visitor = new CapturingSelectVisitor();
select.visit(visitor);
assertThat(visitor.enter).containsSequence(SelectTop.create(10), foo, new From(table), table);
assertThat(visitor.enter).containsSequence(foo, table, new From(table), table);
assertThat(select.getLimit()).isEqualTo(OptionalLong.of(10));
}
@Test // DATAJDBC-309
@@ -112,12 +114,9 @@ public class SelectBuilderUnitTests {
Column name = employee.column("name").as("emp_name");
Column department_name = employee.column("name").as("department_name");
Select select = builder.select(name, department_name).from(employee)
.join(department)
.on(SQL.column("department_id", employee))
.equals(SQL.column("id", department))
.and(SQL.column("tenant", employee))
.equals(SQL.column("tenant", department))
Select select = builder.select(name, department_name).from(employee).join(department)
.on(SQL.column("department_id", employee)).equals(SQL.column("id", department))
.and(SQL.column("tenant", employee)).equals(SQL.column("tenant", department))
.orderBy(OrderByField.from(name).asc()).build();
CapturingSelectVisitor visitor = new CapturingSelectVisitor();
@@ -128,11 +127,11 @@ public class SelectBuilderUnitTests {
Join join = visitor.enter.stream().filter(Join.class::isInstance).map(Join.class::cast).findFirst().get();
assertThat(join.getJoinTable()).isEqualTo(department);
assertThat(join.getOn().toString()).isEqualTo(new SimpleSegment("employee.department_id = department.id AND employee.tenant = department.tenant").toString());
assertThat(join.getOn().toString()).isEqualTo(
new SimpleSegment("employee.department_id = department.id AND employee.tenant = department.tenant").toString());
assertThat(join.getType()).isEqualTo(JoinType.JOIN);
}
static class CapturingSelectVisitor implements Visitor {
final List<Visitable> enter = new ArrayList<>();

View File

@@ -33,7 +33,8 @@ public class SelectValidatorUnitTests {
assertThatThrownBy(() -> {
SQL.newSelect(column).from(SQL.table("bar")).build();
}).isInstanceOf(IllegalStateException.class).hasMessageContaining("Required table [table] by a SELECT column not imported by FROM [bar] or JOIN []");
}).isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Required table [table] by a SELECT column not imported by FROM [bar] or JOIN []");
}
@Test // DATAJDBC-309
@@ -43,7 +44,8 @@ public class SelectValidatorUnitTests {
assertThatThrownBy(() -> {
SQL.newSelect(Functions.count(column)).from(SQL.table("bar")).build();
}).isInstanceOf(IllegalStateException.class).hasMessageContaining("Required table [table] by a SELECT column not imported by FROM [bar] or JOIN []");
}).isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Required table [table] by a SELECT column not imported by FROM [bar] or JOIN []");
}
@Test // DATAJDBC-309
@@ -52,31 +54,38 @@ public class SelectValidatorUnitTests {
Column column = SQL.table("table").column("foo");
assertThatThrownBy(() -> {
SQL.newSelect(Functions.distinct(column)).from(SQL.table("bar")).build();
}).isInstanceOf(IllegalStateException.class).hasMessageContaining("Required table [table] by a SELECT column not imported by FROM [bar] or JOIN []");
SQL.newSelect(column).distinct().from(SQL.table("bar")).build();
}).isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Required table [table] by a SELECT column not imported by FROM [bar] or JOIN []");
}
@Test // DATAJDBC-309
public void shouldReportMissingTableViaOrderBy() {
Column column = SQL.table("table").column("foo");
Column foo = SQL.table("table").column("foo");
Table bar = SQL.table("bar");
assertThatThrownBy(() -> {
SQL.newSelect(SQL.column("foo")) //
.from(SQL.table("bar")).orderBy(column) //
SQL.newSelect(bar.column("foo")) //
.from(bar) //
.orderBy(foo) //
.build();
}).isInstanceOf(IllegalStateException.class).hasMessageContaining("Required table [table] by a ORDER BY column not imported by FROM [bar] or JOIN []");
}).isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Required table [table] by a ORDER BY column not imported by FROM [bar] or JOIN []");
}
@Test // DATAJDBC-309
public void shouldReportMissingTableViaWhere() {
Column column = SQL.table("table").column("foo");
Table bar = SQL.table("bar");
assertThatThrownBy(() -> {
SQL.newSelect(SQL.column("foo")).from(SQL.table("bar")) //
SQL.newSelect(bar.column("foo")) //
.from(bar) //
.where(new SimpleCondition(column, "=", "foo")) //
.build();
}).isInstanceOf(IllegalStateException.class).hasMessageContaining("Required table [table] by a WHERE predicate not imported by FROM [bar] or JOIN []");
}).isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Required table [table] by a WHERE predicate not imported by FROM [bar] or JOIN []");
}
}