Support for in line queries for tables.

`InlineQuery` can be used wherever a `Table` was used up to now.

```
Table one = ...;
Select select = Select.builder()
		.select(one.column("id"), employee.column("name"))
		.from(one)
		.build();

InlineQuery inline = InlineQuery.create(select, "inline");
Select select = Select.builder()
		.select(inline.column("id"), inline.column("name"))
		.from(inline)
		.build();
```

Join and From renderer now use the same FromTableVisitor.
Also the SelectListVisitor reuses now the ExpressionVisitor.

Fixes #1003
Original pull request: #1018.
This commit is contained in:
Jens Schauder
2021-07-23 16:48:51 +02:00
committed by Mark Paluch
parent 0d03b5928b
commit f6eacf327c
30 changed files with 1200 additions and 286 deletions

View File

@@ -121,7 +121,7 @@ class QueryMapper {
Column column = (Column) expression;
Field field = createPropertyField(entity, column.getName());
Table table = column.getTable();
TableLike table = column.getTable();
Assert.state(table != null, String.format("The column %s must have a table set.", column));

View File

@@ -23,11 +23,11 @@ import java.util.Set;
import java.util.function.Consumer;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing;
import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting;
import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.TableLike;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -139,7 +139,7 @@ public class PostgresDialect extends AbstractDialect {
@Override
public String getLock(LockOptions lockOptions) {
List<Table> tables = lockOptions.getFrom().getTables();
List<TableLike> tables = lockOptions.getFrom().getTables();
if (tables.isEmpty()) {
return "";
}

View File

@@ -18,13 +18,7 @@ package org.springframework.data.relational.core.sql;
/**
* {@link Segment} to select all columns from a {@link Table}.
* <p/>
* * Renders to: {@code
*
<table>
* .*} as in {@code SELECT
*
<table>
* .* FROM …}.
* Renders to: {@code <table>.*} as in {@code SELECT <table>.* FROM …}.
*
* @author Mark Paluch
* @since 1.1
@@ -32,9 +26,9 @@ package org.springframework.data.relational.core.sql;
*/
public class AsteriskFromTable extends AbstractSegment implements Expression {
private final Table table;
private final TableLike table;
AsteriskFromTable(Table table) {
AsteriskFromTable(TableLike table) {
super(table);
this.table = table;
}
@@ -46,7 +40,7 @@ public class AsteriskFromTable extends AbstractSegment implements Expression {
/**
* @return the associated {@link Table}.
*/
public Table getTable() {
public TableLike getTable() {
return table;
}

View File

@@ -30,9 +30,9 @@ import org.springframework.util.Assert;
public class Column extends AbstractSegment implements Expression, Named {
private final SqlIdentifier name;
private final Table table;
private final TableLike table;
Column(String name, Table table) {
Column(String name, TableLike table) {
super(table);
Assert.notNull(name, "Name must not be null");
@@ -41,7 +41,7 @@ public class Column extends AbstractSegment implements Expression, Named {
this.table = table;
}
Column(SqlIdentifier name, Table table) {
Column(SqlIdentifier name, TableLike table) {
super(table);
Assert.notNull(name, "Name must not be null");
@@ -57,7 +57,7 @@ public class Column extends AbstractSegment implements Expression, Named {
* @param table the table, must not be {@literal null}.
* @return the new {@link Column}.
*/
public static Column create(String name, Table table) {
public static Column create(String name, TableLike table) {
Assert.hasText(name, "Name must not be null or empty");
Assert.notNull(table, "Table must not be null");
@@ -341,7 +341,7 @@ public class Column extends AbstractSegment implements Expression, Named {
* {@link Table}.
*/
@Nullable
public Table getTable() {
public TableLike getTable() {
return table;
}
@@ -370,12 +370,12 @@ public class Column extends AbstractSegment implements Expression, Named {
private final SqlIdentifier alias;
private AliasedColumn(String name, Table table, String alias) {
private AliasedColumn(String name, TableLike table, String alias) {
super(name, table);
this.alias = SqlIdentifier.unquoted(alias);
}
private AliasedColumn(SqlIdentifier name, Table table, SqlIdentifier alias) {
private AliasedColumn(SqlIdentifier name, TableLike table, SqlIdentifier alias) {
super(name, table);
this.alias = alias;
}

View File

@@ -42,7 +42,7 @@ class DefaultSelect implements Select {
private final List<OrderByField> orderBy;
private final @Nullable LockMode lockMode;
DefaultSelect(boolean distinct, List<Expression> selectList, List<Table> from, long limit, long offset,
DefaultSelect(boolean distinct, List<Expression> selectList, List<TableLike> from, long limit, long offset,
List<Join> joins, @Nullable Condition where, List<OrderByField> orderBy, @Nullable LockMode lockMode) {
this.distinct = distinct;

View File

@@ -38,7 +38,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
private boolean distinct = false;
private List<Expression> selectList = new ArrayList<>();
private List<Table> from = new ArrayList<>();
private List<TableLike> from = new ArrayList<>();
private long limit = -1;
private long offset = -1;
private List<Join> joins = new ArrayList<>();
@@ -107,7 +107,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectAndFrom#from(org.springframework.data.relational.core.sql.Table)
*/
@Override
public SelectFromAndJoin from(Table table) {
public SelectFromAndJoin from(TableLike table) {
from.add(table);
return this;
}
@@ -117,7 +117,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectAndFrom#from(org.springframework.data.relational.core.sql.Table[])
*/
@Override
public SelectFromAndJoin from(Table... tables) {
public SelectFromAndJoin from(TableLike... tables) {
from.addAll(Arrays.asList(tables));
return this;
}
@@ -127,7 +127,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectAndFrom#from(java.util.Collection)
*/
@Override
public SelectFromAndJoin from(Collection<? extends Table> tables) {
public SelectFromAndJoin from(Collection<? extends TableLike> tables) {
from.addAll(tables);
return this;
}
@@ -248,7 +248,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin#join(org.springframework.data.relational.core.sql.Table)
*/
@Override
public SelectOn join(Table table) {
public SelectOn join(TableLike table) {
return new JoinBuilder(table, this);
}
@@ -257,7 +257,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin#join(org.springframework.data.relational.core.sql.Table)
*/
@Override
public SelectOn leftOuterJoin(Table table) {
public SelectOn leftOuterJoin(TableLike table) {
return new JoinBuilder(table, this, JoinType.LEFT_OUTER_JOIN);
}
@@ -295,21 +295,21 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
*/
static class JoinBuilder implements SelectOn, SelectOnConditionComparison, SelectFromAndJoinCondition {
private final Table table;
private final TableLike table;
private final DefaultSelectBuilder selectBuilder;
private final JoinType joinType;
private @Nullable Expression from;
private @Nullable Expression to;
private @Nullable Condition condition;
JoinBuilder(Table table, DefaultSelectBuilder selectBuilder, JoinType joinType) {
JoinBuilder(TableLike table, DefaultSelectBuilder selectBuilder, JoinType joinType) {
this.table = table;
this.selectBuilder = selectBuilder;
this.joinType = joinType;
}
JoinBuilder(Table table, DefaultSelectBuilder selectBuilder) {
JoinBuilder(TableLike table, DefaultSelectBuilder selectBuilder) {
this(table, selectBuilder, JoinType.JOIN);
}
@@ -417,7 +417,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin#join(org.springframework.data.relational.core.sql.Table)
*/
@Override
public SelectOn join(Table table) {
public SelectOn join(TableLike table) {
selectBuilder.join(finishJoin());
return selectBuilder.join(table);
}
@@ -427,7 +427,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
* @see org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin#leftOuterJoin(org.springframework.data.relational.core.sql.Table)
*/
@Override
public SelectOn leftOuterJoin(Table table) {
public SelectOn leftOuterJoin(TableLike table) {
selectBuilder.join(finishJoin());
return selectBuilder.leftOuterJoin(table);
}

View File

@@ -29,20 +29,20 @@ import org.springframework.util.StringUtils;
*/
public class From extends AbstractSegment {
private final List<Table> tables;
private final List<TableLike> tables;
From(Table... tables) {
From(TableLike... tables) {
this(Arrays.asList(tables));
}
From(List<Table> tables) {
From(List<TableLike> tables) {
super(tables.toArray(new Table[] {}));
super(tables.toArray(new TableLike[] {}));
this.tables = Collections.unmodifiableList(tables);
}
public List<Table> getTables() {
public List<TableLike> getTables() {
return this.tables;
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2019-2021 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
*
* 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.
*/
package org.springframework.data.relational.core.sql;
import org.springframework.util.Assert;
/**
* Represents a inline query within a SQL statement. Typically used in {@code FROM} or {@code JOIN} clauses.
* <p/>
* Renders to: {@code (<SELECT>) AS <ALIAS>} in a from or join clause, and to {@code <ALIAS>} when used in an
* expression.
* <p/>
* Note that this does not implement {@link Aliased} because the Alias is not optional but required and therefore more
* like a name although the SQL term is "alias".
*
* @author Jens Schauder
* @since 2.3
*/
public class InlineQuery extends AbstractSegment implements TableLike {
private final Select select;
private final SqlIdentifier alias;
InlineQuery(Select select, SqlIdentifier alias) {
super(select);
this.select = select;
this.alias = alias;
}
/**
* Creates a new {@link InlineQuery} using an {@code alias}.
*
* @param select must not be {@literal null}.
* @param alias must not be {@literal null} or empty.
* @return the new {@link InlineQuery} using the {@code alias}.
*/
public static InlineQuery create(Select select, SqlIdentifier alias) {
Assert.notNull(select, "Select must not be null!");
Assert.notNull(alias, "Alias must not be null or empty!");
return new InlineQuery(select, alias);
}
/**
* Creates a new {@link InlineQuery} using an {@code alias}.
*
* @param select must not be {@literal null} or empty.
* @param alias must not be {@literal null} or empty.
* @return the new {@link InlineQuery} using the {@code alias}.
*/
public static InlineQuery create(Select select, String alias) {
return create(select, SqlIdentifier.unquoted(alias));
}
/**
* @return the table name.
*/
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Named#getName()
*/
public SqlIdentifier getName() {
return alias;
}
/**
* @return the table name as it is used in references. This can be the actual {@link #getName() name} or an
* {@link Aliased#getAlias() alias}.
*/
public SqlIdentifier getReferenceName() {
return alias;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "(" + select + ") AS " + alias;
}
}

View File

@@ -29,10 +29,10 @@ package org.springframework.data.relational.core.sql;
public class Join extends AbstractSegment {
private final JoinType type;
private final Table joinTable;
private final TableLike joinTable;
private final Condition on;
Join(JoinType type, Table joinTable, Condition on) {
Join(JoinType type, TableLike joinTable, Condition on) {
super(joinTable, on);
@@ -51,7 +51,7 @@ public class Join extends AbstractSegment {
/**
* @return the joined {@link Table}.
*/
public Table getJoinTable() {
public TableLike getJoinTable() {
return joinTable;
}

View File

@@ -134,7 +134,7 @@ public interface SelectBuilder {
* @see SQL#table(String)
*/
@Override
SelectFromAndJoin from(Table table);
SelectFromAndJoin from(TableLike table);
/**
* Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods
@@ -146,7 +146,7 @@ public interface SelectBuilder {
* @see SQL#table(String)
*/
@Override
SelectFromAndJoin from(Table... tables);
SelectFromAndJoin from(TableLike... tables);
/**
* Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods
@@ -158,7 +158,7 @@ public interface SelectBuilder {
* @see SQL#table(String)
*/
@Override
SelectFromAndJoin from(Collection<? extends Table> tables);
SelectFromAndJoin from(Collection<? extends TableLike> tables);
}
/**
@@ -186,7 +186,7 @@ public interface SelectBuilder {
* @see From
* @see SQL#table(String)
*/
SelectFromAndOrderBy from(Table table);
SelectFromAndOrderBy from(TableLike table);
/**
* Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods
@@ -197,7 +197,7 @@ public interface SelectBuilder {
* @see From
* @see SQL#table(String)
*/
SelectFromAndOrderBy from(Table... tables);
SelectFromAndOrderBy from(TableLike... tables);
/**
* Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods
@@ -208,7 +208,7 @@ public interface SelectBuilder {
* @see From
* @see SQL#table(String)
*/
SelectFromAndOrderBy from(Collection<? extends Table> tables);
SelectFromAndOrderBy from(Collection<? extends TableLike> tables);
}
/**
@@ -229,13 +229,13 @@ public interface SelectBuilder {
SelectFromAndOrderBy from(String table);
@Override
SelectFromAndOrderBy from(Table table);
SelectFromAndOrderBy from(TableLike table);
@Override
SelectFromAndOrderBy from(Table... tables);
SelectFromAndOrderBy from(TableLike... tables);
@Override
SelectFromAndOrderBy from(Collection<? extends Table> tables);
SelectFromAndOrderBy from(Collection<? extends TableLike> tables);
@Override
SelectFromAndOrderBy orderBy(Column... columns);
@@ -263,7 +263,7 @@ public interface SelectBuilder {
* @see SQL#table(String)
*/
@Override
SelectFromAndJoin from(Table table);
SelectFromAndJoin from(TableLike table);
/**
* Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods
@@ -275,7 +275,7 @@ public interface SelectBuilder {
* @see SQL#table(String)
*/
@Override
SelectFromAndJoin from(Table... tables);
SelectFromAndJoin from(TableLike... tables);
/**
* Declare one or more {@link Table}s to {@code SELECT … FROM}. Multiple calls to this or other {@code from} methods
@@ -287,7 +287,7 @@ public interface SelectBuilder {
* @see SQL#table(String)
*/
@Override
SelectFromAndJoin from(Collection<? extends Table> tables);
SelectFromAndJoin from(Collection<? extends TableLike> tables);
/**
* Apply {@code limit} and {@code offset} parameters to the select statement. To read the first 20 rows from start
@@ -474,17 +474,17 @@ public interface SelectBuilder {
* @see Join
* @see SQL#table(String)
*/
SelectOn join(Table table);
SelectOn join(TableLike table);
/**
* Declare a {@code LEFT OUTER JOIN} {@link Table}.
*
* @param table name of the table, must not be {@literal null}.
* @param table must not be {@literal null}.
* @return {@code this} builder.
* @see Join
* @see SQL#table(String)
*/
SelectOn leftOuterJoin(Table table);
SelectOn leftOuterJoin(TableLike table);
}
/**

View File

@@ -34,10 +34,10 @@ class SelectValidator extends AbstractImportValidator {
private final Stack<Select> selects = new Stack<>();
private int selectFieldCount;
private Set<Table> requiredBySelect = new HashSet<>();
private Set<Table> requiredByOrderBy = new HashSet<>();
private Set<TableLike> requiredBySelect = new HashSet<>();
private Set<TableLike> requiredByOrderBy = new HashSet<>();
private Set<Table> join = new HashSet<>();
private Set<TableLike> join = new HashSet<>();
/**
* Validates a {@link Select} statement.
@@ -57,7 +57,7 @@ class SelectValidator extends AbstractImportValidator {
throw new IllegalStateException("SELECT does not declare a select list");
}
for (Table table : requiredBySelect) {
for (TableLike table : requiredBySelect) {
if (!join.contains(table) && !from.contains(table)) {
throw new IllegalStateException(String
.format("Required table [%s] by a SELECT column not imported by FROM %s or JOIN %s", table, from, join));
@@ -71,7 +71,7 @@ class SelectValidator extends AbstractImportValidator {
}
}
for (Table table : requiredByOrderBy) {
for (TableLike table : requiredByOrderBy) {
if (!join.contains(table) && !from.contains(table)) {
throw new IllegalStateException(String
.format("Required table [%s] by a ORDER BY column not imported by FROM %s or JOIN %s", table, from, join));
@@ -100,13 +100,13 @@ class SelectValidator extends AbstractImportValidator {
if (segment instanceof AsteriskFromTable && parent instanceof Select) {
Table table = ((AsteriskFromTable) segment).getTable();
TableLike table = ((AsteriskFromTable) segment).getTable();
requiredBySelect.add(table);
}
if (segment instanceof Column && (parent instanceof Select || parent instanceof SimpleFunction)) {
Table table = ((Column) segment).getTable();
TableLike table = ((Column) segment).getTable();
if (table != null) {
requiredBySelect.add(table);
@@ -115,15 +115,15 @@ class SelectValidator extends AbstractImportValidator {
if (segment instanceof Column && parent instanceof OrderByField) {
Table table = ((Column) segment).getTable();
TableLike table = ((Column) segment).getTable();
if (table != null) {
requiredByOrderBy.add(table);
}
}
if (segment instanceof Table && parent instanceof Join) {
join.add((Table) segment);
if (segment instanceof TableLike && parent instanceof Join) {
join.add((TableLike) segment);
}
super.enter(segment);
}

View File

@@ -15,15 +15,10 @@
*/
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;
/**
* Represents a table reference within an SQL statement. Typically used to denote {@code FROM} or {@code JOIN} or to
* Represents a table reference within a SQL statement. Typically used to denote {@code FROM} or {@code JOIN} or to
* prefix a {@link Column}.
* <p/>
* Renders to: {@code <name>} or {@code <name> AS <name>}.
@@ -31,7 +26,7 @@ import org.springframework.util.Assert;
* @author Mark Paluch
* @since 1.1
*/
public class Table extends AbstractSegment {
public class Table extends AbstractSegment implements TableLike {
private final SqlIdentifier name;
@@ -112,110 +107,6 @@ public class Table extends AbstractSegment {
return new AliasedTable(name, alias);
}
/**
* Creates a new {@link Column} associated with this {@link Table}.
* <p/>
* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
* {@link Column}s that were created for this table.
*
* @param name column name, must not be {@literal null} or empty.
* @return a new {@link Column} associated with this {@link Table}.
*/
public Column column(String name) {
Assert.hasText(name, "Name must not be null or empty!");
return new Column(name, this);
}
/**
* Creates a new {@link Column} associated with this {@link Table}.
* <p/>
* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
* {@link Column}s that were created for this table.
*
* @param name column name, must not be {@literal null} or empty.
* @return a new {@link Column} associated with this {@link Table}.
* @since 2.0
*/
public Column column(SqlIdentifier name) {
Assert.notNull(name, "Name must not be null");
return new Column(name, this);
}
/**
* Creates a {@link List} of {@link Column}s associated with this {@link Table}.
* <p/>
* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
* {@link Column}s that were created for this table.
*
* @param names column names, must not be {@literal null} or empty.
* @return a new {@link List} of {@link Column}s associated with this {@link Table}.
*/
public List<Column> columns(String... names) {
Assert.notNull(names, "Names must not be null");
return columns(Arrays.asList(names));
}
/**
* Creates a {@link List} of {@link Column}s associated with this {@link Table}.
* <p/>
* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
* {@link Column}s that were created for this table.
*
* @param names column names, must not be {@literal null} or empty.
* @return a new {@link List} of {@link Column}s associated with this {@link Table}.
* @since 2.0
*/
public List<Column> columns(SqlIdentifier... names) {
Assert.notNull(names, "Names must not be null");
List<Column> columns = new ArrayList<>();
for (SqlIdentifier name : names) {
columns.add(column(name));
}
return columns;
}
/**
* Creates a {@link List} of {@link Column}s associated with this {@link Table}.
* <p/>
* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
* {@link Column}s that were created for this table.
*
* @param names column names, must not be {@literal null} or empty.
* @return a new {@link List} of {@link Column}s associated with this {@link Table}.
*/
public List<Column> columns(Collection<String> names) {
Assert.notNull(names, "Names must not be null");
List<Column> columns = new ArrayList<>();
for (String name : names) {
columns.add(column(name));
}
return columns;
}
/**
* Creates a {@link AsteriskFromTable} maker selecting all columns from this {@link Table} (e.g. {@code SELECT
*
<table>
* .*}.
*
* @return the select all marker for this {@link Table}.
*/
public AsteriskFromTable asterisk() {
return new AsteriskFromTable(this);
}
/**
* @return the table name.
*/

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2021 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
*
* 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.
*/
package org.springframework.data.relational.core.sql;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* A segment that can be used as table in a query.
*
* @author Jens Schauder
* @Since 2.3
*/
public interface TableLike extends Segment {
/**
* Creates a new {@link Column} associated with this {@link Table}.
* <p/>
* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
* {@link Column}s that were created for this table.
*
* @param name column name, must not be {@literal null} or empty.
* @return a new {@link Column} associated with this {@link Table}.
*/
default Column column(String name) {
Assert.hasText(name, "Name must not be null or empty!");
return new Column(name, this);
}
/**
* Creates a new {@link Column} associated with this {@link Table}.
* <p/>
* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
* {@link Column}s that were created for this table.
*
* @param name column name, must not be {@literal null} or empty.
* @return a new {@link Column} associated with this {@link Table}.
* @since 2.0
*/
default Column column(SqlIdentifier name) {
Assert.notNull(name, "Name must not be null");
return new Column(name, this);
}
/**
* Creates a {@link List} of {@link Column}s associated with this {@link Table}.
* <p/>
* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
* {@link Column}s that were created for this table.
*
* @param names column names, must not be {@literal null} or empty.
* @return a new {@link List} of {@link Column}s associated with this {@link Table}.
*/
default List<Column> columns(String... names) {
Assert.notNull(names, "Names must not be null");
return columns(Arrays.asList(names));
}
/**
* Creates a {@link List} of {@link Column}s associated with this {@link Table}.
* <p/>
* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
* {@link Column}s that were created for this table.
*
* @param names column names, must not be {@literal null} or empty.
* @return a new {@link List} of {@link Column}s associated with this {@link Table}.
* @since 2.0
*/
default List<Column> columns(SqlIdentifier... names) {
Assert.notNull(names, "Names must not be null");
List<Column> columns = new ArrayList<>();
for (SqlIdentifier name : names) {
columns.add(column(name));
}
return columns;
}
/**
* Creates a {@link List} of {@link Column}s associated with this {@link Table}.
* <p/>
* Note: This {@link Table} does not track column creation and there is no possibility to enumerate all
* {@link Column}s that were created for this table.
*
* @param names column names, must not be {@literal null} or empty.
* @return a new {@link List} of {@link Column}s associated with this {@link Table}.
*/
default List<Column> columns(Collection<String> names) {
Assert.notNull(names, "Names must not be null");
List<Column> columns = new ArrayList<>();
for (String name : names) {
columns.add(column(name));
}
return columns;
}
/**
* Creates a {@link AsteriskFromTable} maker selecting all columns from this {@link Table} (e.g. {@code SELECT
*
<table>
* .*}.
*
* @return the select all marker for this {@link Table}.
*/
default AsteriskFromTable asterisk() {
return new AsteriskFromTable(this);
}
SqlIdentifier getName();
SqlIdentifier getReferenceName();
}

View File

@@ -17,12 +17,14 @@ package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.TableLike;
import org.springframework.data.relational.core.sql.Visitable;
import org.springframework.lang.Nullable;
/**
* Renderer for {@link Column}s.
* Renderer for {@link Column}s. Renders a column as {@literal
* <table>
* .<column>} or {@literal <column>}.
*
* @author Mark Paluch
* @since 1.1
@@ -65,8 +67,8 @@ class ColumnVisitor extends TypedSubtreeVisitor<Column> {
@Override
Delegation leaveNested(Visitable segment) {
if (segment instanceof Table) {
tableName = context.getNamingStrategy().getReferenceName((Table) segment);
if (segment instanceof TableLike) {
tableName = context.getNamingStrategy().getReferenceName((TableLike) segment);
}
return super.leaveNested(segment);

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.sql.AsteriskFromTable;
import org.springframework.data.relational.core.sql.BindMarker;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.Condition;
@@ -24,6 +25,7 @@ import org.springframework.data.relational.core.sql.SimpleFunction;
import org.springframework.data.relational.core.sql.SubselectExpression;
import org.springframework.data.relational.core.sql.Visitable;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@link PartRenderer} for {@link Expression}s.
@@ -37,12 +39,33 @@ import org.springframework.lang.Nullable;
class ExpressionVisitor extends TypedSubtreeVisitor<Expression> implements PartRenderer {
private final RenderContext context;
private final AliasHandling aliasHandling;
private CharSequence value = "";
private @Nullable PartRenderer partRenderer;
/**
* Creates an {@code ExpressionVisitor} that does not use aliases for column names
* @param context must not be {@literal null}.
*/
ExpressionVisitor(RenderContext context) {
this(context, AliasHandling.IGNORE);
}
/**
* Creates an {@code ExpressionVisitor}.
*
* @param context must not be {@literal null}.
* @param aliasHandling controls if columns should be rendered as their alias or using their table names.
* @since 2.3
*/
ExpressionVisitor(RenderContext context, AliasHandling aliasHandling) {
Assert.notNull(context, "The render context must not be null");
Assert.notNull(aliasHandling, "The aliasHandling must not be null");
this.context = context;
this.aliasHandling = aliasHandling;
}
/*
@@ -70,7 +93,8 @@ class ExpressionVisitor extends TypedSubtreeVisitor<Expression> implements PartR
Column column = (Column) segment;
value = NameRenderer.fullyQualifiedReference(context, column);
value = aliasHandling == AliasHandling.USE ? NameRenderer.fullyQualifiedReference(context, column)
: NameRenderer.fullyQualifiedUnaliasedReference(context, column);
} else if (segment instanceof BindMarker) {
if (segment instanceof Named) {
@@ -78,7 +102,10 @@ class ExpressionVisitor extends TypedSubtreeVisitor<Expression> implements PartR
} else {
value = segment.toString();
}
} else { // works for Literal and SimpleExpression and possibly more
} else if (segment instanceof AsteriskFromTable) {
value = NameRenderer.render(context, ((AsteriskFromTable) segment).getTable()) + ".*";
} else {
// works for literals and just and possibly more
value = segment.toString();
}
@@ -93,6 +120,7 @@ class ExpressionVisitor extends TypedSubtreeVisitor<Expression> implements PartR
Delegation enterNested(Visitable segment) {
if (segment instanceof Condition) {
ConditionVisitor visitor = new ConditionVisitor(context);
partRenderer = visitor;
return Delegation.delegateTo(visitor);
@@ -124,4 +152,20 @@ class ExpressionVisitor extends TypedSubtreeVisitor<Expression> implements PartR
public CharSequence getRenderedPart() {
return value;
}
/**
* Describes how aliases of columns should be rendered.
* @since 2.3
*/
enum AliasHandling {
/**
* The alias does not get used.
*/
IGNORE,
/**
* The alias gets used. This means aliased columns get rendered as {@literal <alias>}.
*/
USE
}
}

View File

@@ -17,20 +17,28 @@ package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.sql.Aliased;
import org.springframework.data.relational.core.sql.From;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.InlineQuery;
import org.springframework.data.relational.core.sql.TableLike;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Renderer for {@link Table} used within a {@link From} clause. Uses a {@link RenderTarget} to call back for render
* Renderer for {@link TableLike} used within a {@link From} or
* {@link org.springframework.data.relational.core.sql.Join} clause. Uses a {@link RenderTarget} to call back for render
* results.
*
* @author Mark Paluch
* @author Jens Schauder
* @since 1.1
*/
class FromTableVisitor extends TypedSubtreeVisitor<Table> {
class FromTableVisitor extends TypedSubtreeVisitor<TableLike> {
private final RenderContext context;
private final RenderTarget parent;
@Nullable
private SelectStatementVisitor delegate;
@Nullable
private StringBuilder builder = null;
FromTableVisitor(RenderContext context, RenderTarget parent) {
super();
@@ -43,9 +51,32 @@ class FromTableVisitor extends TypedSubtreeVisitor<Table> {
* @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable)
*/
@Override
Delegation enterMatched(Table segment) {
Delegation enterMatched(TableLike segment) {
StringBuilder builder = new StringBuilder();
builder = new StringBuilder();
if (segment instanceof InlineQuery) {
builder.append("(");
delegate = new SelectStatementVisitor(context);
return Delegation.delegateTo(delegate);
}
return super.enterMatched(segment);
}
@Override
Delegation leaveMatched(TableLike segment) {
Assert.state(builder != null, "Builder must not be null in leaveMatched.");
if (delegate != null) {
builder.append(delegate.getRenderedPart());
builder.append(") ");
delegate = null;
}
builder.append(NameRenderer.render(context, segment));
if (segment instanceof Aliased) {
@@ -54,6 +85,6 @@ class FromTableVisitor extends TypedSubtreeVisitor<Table> {
parent.onRendered(builder);
return super.enterMatched(segment);
return super.leaveMatched(segment);
}
}

View File

@@ -15,10 +15,9 @@
*/
package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.sql.Aliased;
import org.springframework.data.relational.core.sql.Condition;
import org.springframework.data.relational.core.sql.Join;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.TableLike;
import org.springframework.data.relational.core.sql.Visitable;
/**
@@ -30,18 +29,18 @@ import org.springframework.data.relational.core.sql.Visitable;
*/
class JoinVisitor extends TypedSubtreeVisitor<Join> {
private final RenderContext context;
private final RenderTarget parent;
private final StringBuilder joinClause = new StringBuilder();
private final FromTableVisitor fromTableVisitor;
private final ConditionVisitor conditionVisitor;
private boolean inCondition = false;
private boolean hasSeenCondition = false;
JoinVisitor(RenderContext context, RenderTarget parent) {
this.context = context;
this.parent = parent;
this.conditionVisitor = new ConditionVisitor(context);
this.fromTableVisitor = new FromTableVisitor(context, joinClause::append);
}
/*
@@ -63,11 +62,8 @@ class JoinVisitor extends TypedSubtreeVisitor<Join> {
@Override
Delegation enterNested(Visitable segment) {
if (segment instanceof Table && !inCondition) {
joinClause.append(NameRenderer.render(context, (Table) segment));
if (segment instanceof Aliased) {
joinClause.append(" ").append(NameRenderer.render(context, (Aliased) segment));
}
if (segment instanceof TableLike && !inCondition) {
return Delegation.delegateTo(fromTableVisitor);
} else if (segment instanceof Condition) {
inCondition = true;
@@ -108,6 +104,7 @@ class JoinVisitor extends TypedSubtreeVisitor<Join> {
*/
@Override
Delegation leaveMatched(Join segment) {
parent.onRendered(joinClause);
return super.leaveMatched(segment);
}

View File

@@ -21,34 +21,28 @@ import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.Named;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.TableLike;
/**
* Utility to render {@link Column} and {@link Table} names using {@link SqlIdentifier} and {@link RenderContext} to
* SQL.
*
* @author Mark Paluch
* @author Jens Schauder
*/
class NameRenderer {
/**
* Render the {@link Table#getName() table name } with considering the {@link RenderNamingStrategy#getName(Table)
* naming strategy}.
*
* @param context
* @param table
* @return
* Render the {@link TableLike#getName() table name } with considering the
* {@link RenderNamingStrategy#getName(TableLike) naming strategy}.
*/
static CharSequence render(RenderContext context, Table table) {
static CharSequence render(RenderContext context, TableLike table) {
return render(context, context.getNamingStrategy().getName(table));
}
/**
* Render the {@link Column#getName() column name} with considering the {@link RenderNamingStrategy#getName(Column)
* naming strategy}.
*
* @param context
* @param table
* @return
*/
static CharSequence render(RenderContext context, Column column) {
return render(context, context.getNamingStrategy().getName(column));
@@ -56,10 +50,6 @@ class NameRenderer {
/**
* Render the {@link Named#getName() name}.
*
* @param context
* @param table
* @return
*/
static CharSequence render(RenderContext context, Named named) {
return render(context, named.getName());
@@ -67,10 +57,6 @@ class NameRenderer {
/**
* Render the {@link Aliased#getAlias() alias}.
*
* @param context
* @param table
* @return
*/
static CharSequence render(RenderContext context, Aliased aliased) {
return render(context, aliased.getAlias());
@@ -78,23 +64,15 @@ class NameRenderer {
/**
* Render the {@link Table#getReferenceName()} table reference name} with considering the
* {@link RenderNamingStrategy#getReferenceName(Table) naming strategy}.
*
* @param context
* @param table
* @return
* {@link RenderNamingStrategy#getReferenceName(TableLike) naming strategy}.
*/
static CharSequence reference(RenderContext context, Table table) {
static CharSequence reference(RenderContext context, TableLike table) {
return render(context, context.getNamingStrategy().getReferenceName(table));
}
/**
* Render the {@link Column#getReferenceName()} column reference name} with considering the
* {@link RenderNamingStrategy#getReferenceName(Column) naming strategy}.
*
* @param context
* @param table
* @return
*/
static CharSequence reference(RenderContext context, Column column) {
return render(context, context.getNamingStrategy().getReferenceName(column));
@@ -103,9 +81,6 @@ class NameRenderer {
/**
* Render the fully-qualified table and column name with considering the naming strategies of each component.
*
* @param context
* @param column
* @return
* @see RenderNamingStrategy#getReferenceName
*/
static CharSequence fullyQualifiedReference(RenderContext context, Column column) {
@@ -116,13 +91,24 @@ class NameRenderer {
namingStrategy.getReferenceName(column)));
}
/**
* Render the fully-qualified table and column name with considering the naming strategies of each component without
* using the alias for the column. For the table the alias is still used.
*
* @see #fullyQualifiedReference(RenderContext, Column)
* @since 2.3
*/
static CharSequence fullyQualifiedUnaliasedReference(RenderContext context, Column column) {
RenderNamingStrategy namingStrategy = context.getNamingStrategy();
return render(context,
SqlIdentifier.from(namingStrategy.getReferenceName(column.getTable()), namingStrategy.getName(column)));
}
/**
* Render the {@link SqlIdentifier#toSql(IdentifierProcessing) identifier to SQL} considering
* {@link IdentifierProcessing}.
*
* @param context
* @param identifier
* @return
*/
static CharSequence render(RenderContext context, SqlIdentifier identifier) {
return identifier.toSql(context.getIdentifierProcessing());

View File

@@ -15,14 +15,15 @@
*/
package org.springframework.data.relational.core.sql.render;
import java.util.Locale;
import java.util.function.Function;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.TableLike;
import org.springframework.util.Assert;
import java.util.Locale;
import java.util.function.Function;
/**
* Factory for {@link RenderNamingStrategy} objects.
*
@@ -110,7 +111,7 @@ public abstract class NamingStrategies {
}
enum AsIs implements RenderNamingStrategy {
INSTANCE;
INSTANCE
}
static class DelegatingRenderNamingStrategy implements RenderNamingStrategy {
@@ -135,12 +136,12 @@ public abstract class NamingStrategies {
}
@Override
public SqlIdentifier getName(Table table) {
public SqlIdentifier getName(TableLike table) {
return delegate.getName(table).transform(mappingFunction::apply);
}
@Override
public SqlIdentifier getReferenceName(Table table) {
public SqlIdentifier getReferenceName(TableLike table) {
return delegate.getReferenceName(table).transform(mappingFunction::apply);
}
}

View File

@@ -20,6 +20,7 @@ import java.util.function.Function;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.TableLike;
import org.springframework.data.relational.core.sql.render.NamingStrategies.DelegatingRenderNamingStrategy;
import org.springframework.util.Assert;
@@ -27,6 +28,7 @@ import org.springframework.util.Assert;
* Naming strategy for SQL rendering.
*
* @author Mark Paluch
* @author Jens Schauder
* @see NamingStrategies
* @since 1.1
*/
@@ -55,24 +57,24 @@ public interface RenderNamingStrategy {
}
/**
* Return the {@link Table#getName() table name}.
* Return the {@link TableLike#getName() table name}.
*
* @param table the table.
* @return the {@link Table#getName() table name}.
* @return the {@link TableLike#getName() table name}.
* @see Table#getName()
*/
default SqlIdentifier getName(Table table) {
default SqlIdentifier getName(TableLike table) {
return table.getName();
}
/**
* Return the {@link Table#getReferenceName() table reference name}.
* Return the {@link TableLike#getReferenceName() table reference name}.
*
* @param table the table.
* @return the {@link Table#getReferenceName() table name}.
* @see Table#getReferenceName()
* @return the {@link TableLike#getReferenceName() table name}.
* @see TableLike#getReferenceName()
*/
default SqlIdentifier getReferenceName(Table table) {
default SqlIdentifier getReferenceName(TableLike table) {
return table.getReferenceName();
}

View File

@@ -15,14 +15,7 @@
*/
package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.sql.Aliased;
import org.springframework.data.relational.core.sql.AsteriskFromTable;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.SelectList;
import org.springframework.data.relational.core.sql.SimpleFunction;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.Visitable;
import org.springframework.data.relational.core.sql.*;
/**
* {@link PartRenderer} for {@link SelectList}s.
@@ -38,11 +31,14 @@ class SelectListVisitor extends TypedSubtreeVisitor<SelectList> implements PartR
private final RenderTarget target;
private boolean requiresComma = false;
private boolean insideFunction = false; // this is hackery and should be fix with a proper visitor for
private ExpressionVisitor expressionVisitor;
// subelements.
SelectListVisitor(RenderContext context, RenderTarget target) {
this.context = context;
this.target = target;
this.expressionVisitor = new ExpressionVisitor(context, ExpressionVisitor.AliasHandling.IGNORE);
}
/*
@@ -56,9 +52,8 @@ class SelectListVisitor extends TypedSubtreeVisitor<SelectList> implements PartR
builder.append(", ");
requiresComma = false;
}
if (segment instanceof SimpleFunction) {
builder.append(((SimpleFunction) segment).getFunctionName()).append("(");
insideFunction = true;
if (segment instanceof Expression) {
return Delegation.delegateTo(expressionVisitor);
}
return super.enterNested(segment);
@@ -82,34 +77,14 @@ class SelectListVisitor extends TypedSubtreeVisitor<SelectList> implements PartR
@Override
Delegation leaveNested(Visitable segment) {
if (segment instanceof Table) {
builder.append(NameRenderer.reference(context, (Table) segment)).append('.');
if (segment instanceof Expression) {
builder.append(expressionVisitor.getRenderedPart());
requiresComma = true;
}
if (segment instanceof SimpleFunction) {
builder.append(")");
if (segment instanceof Aliased) {
builder.append(" AS ").append(NameRenderer.render(context, (Aliased) segment));
}
insideFunction = false;
requiresComma = true;
} else if (segment instanceof AsteriskFromTable) {
builder.append("*");
requiresComma = true;
} else if (segment instanceof Column) {
builder.append(NameRenderer.render(context, (Column) segment));
if (segment instanceof Aliased && !insideFunction) {
builder.append(" AS ").append(NameRenderer.render(context, (Aliased) segment));
}
requiresComma = true;
} else if (segment instanceof AsteriskFromTable) {
// the toString of AsteriskFromTable includes the table name, which would cause it to appear twice.
builder.append("*");
} else if (segment instanceof Expression) {
builder.append(segment.toString());
if (segment instanceof Aliased && !insideFunction) {
builder.append(" AS ").append(NameRenderer.render(context, (Aliased) segment));
}
return super.leaveNested(segment);
@@ -123,4 +98,6 @@ class SelectListVisitor extends TypedSubtreeVisitor<SelectList> implements PartR
public CharSequence getRenderedPart() {
return builder;
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2021 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
*
* 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.
*/
package org.springframework.data.relational.core.sql;
/**
* Public {@link AbstractSegment} for usage in tests in other packages.
*
* @author Jens Schauder
*/
public class AbstractTestSegment extends AbstractSegment{
protected AbstractTestSegment(Segment... children) {
super(children);
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2021 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
*
* 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.
*/
package org.springframework.data.relational.core.sql;
/**
* A variant of {@link From} that can be used in tests in other packages.
*
* @author Jens Schauder
*/
public class TestFrom extends From {
public TestFrom(TableLike... tables) {
super(tables);
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2021 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
*
* 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.
*/
package org.springframework.data.relational.core.sql;
/**
* Public {@link Join} with public constructor for tests in other packages.
*
* @author Jens Schauder
*/
public class TestJoin extends Join {
public TestJoin(JoinType type, TableLike joinTable, Condition on) {
super(type, joinTable, on);
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2021 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
*
* 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.
*/
package org.springframework.data.relational.core.sql.render;
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.Expressions;
import org.springframework.data.relational.core.sql.Functions;
import org.springframework.data.relational.core.sql.SQL;
import org.springframework.data.relational.core.sql.SimpleFunction;
import org.springframework.data.relational.core.sql.Table;
/**
* Tests for the {@link ExpressionVisitor}.
*
* @author Jens Schauder
*/
public class ExpressionVisitorUnitTests {
static SimpleRenderContext simpleRenderContext = new SimpleRenderContext(NamingStrategies.asIs());
@ParameterizedTest // GH-1003
@MethodSource
void expressionsWithOutAliasGetRendered(Fixture f) {
ExpressionVisitor visitor = new ExpressionVisitor(simpleRenderContext);
f.expression.visit(visitor);
assertThat(visitor.getRenderedPart().toString()).as(f.comment).isEqualTo(f.renderResult);
}
static List<Fixture> expressionsWithOutAliasGetRendered() {
// final Select select = Select.builder().select(Functions.count(Expressions.asterisk()),
// SQL.nullLiteral()).build();
return asList( //
fixture("String literal", SQL.literalOf("one"), "'one'"), //
fixture("Numeric literal", SQL.literalOf(23L), "23"), //
fixture("Boolean literal", SQL.literalOf(true), "TRUE"), //
fixture("Just", SQL.literalOf(Expressions.just("just an arbitrary String")), "just an arbitrary String"), //
fixture("Column", Column.create("col", Table.create("tab")), "tab.col"), //
fixture("*", Expressions.asterisk(), "*"), //
fixture("tab.*", Expressions.asterisk(Table.create("tab")), "tab.*"), //
fixture("Count 1", Functions.count(SQL.literalOf(1)), "COUNT(1)"), //
fixture("Count *", Functions.count(Expressions.asterisk()), "COUNT(*)"), //
fixture("Function", SimpleFunction.create("Function", asList(SQL.literalOf("one"), SQL.literalOf("two"))), //
"Function('one', 'two')"), //
fixture("Null", SQL.nullLiteral(), "NULL")); //
}
@Test // GH-1003
void renderAliasedExpressionWithAliasHandlingUse() {
ExpressionVisitor visitor = new ExpressionVisitor(simpleRenderContext, ExpressionVisitor.AliasHandling.USE);
Column expression = Column.aliased("col", Table.create("tab"), "col_alias");
expression.visit(visitor);
assertThat(visitor.getRenderedPart().toString()).isEqualTo("tab.col_alias");
}
@Test // GH-1003
void renderAliasedExpressionWithAliasHandlingDeclare() {
ExpressionVisitor visitor = new ExpressionVisitor(simpleRenderContext, ExpressionVisitor.AliasHandling.IGNORE);
Column expression = Column.aliased("col", Table.create("tab"), "col_alias");
expression.visit(visitor);
assertThat(visitor.getRenderedPart().toString()).isEqualTo("tab.col");
}
@Test // GH-1003
void considersNamingStrategy() {
ExpressionVisitor visitor = new ExpressionVisitor(new SimpleRenderContext(NamingStrategies.toUpper()));
Column expression = Column.create("col", Table.create("tab"));
expression.visit(visitor);
assertThat(visitor.getRenderedPart().toString()).isEqualTo("TAB.COL");
}
@Test // GH-1003
void considerNamingStrategyForTableAsterisk() {
ExpressionVisitor visitor = new ExpressionVisitor(new SimpleRenderContext(NamingStrategies.toUpper()));
Expression expression = Table.create("tab").asterisk();
expression.visit(visitor);
assertThat(visitor.getRenderedPart().toString()).isEqualTo("TAB.*");
}
static Fixture fixture(String comment, Expression expression, String renderResult) {
Fixture f = new Fixture();
f.comment = comment;
f.expression = expression;
f.renderResult = renderResult;
return f;
}
static class Fixture {
String comment;
Expression expression;
String renderResult;
@Override
public String toString() {
return comment;
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2021 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
*
* 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.
*/
package org.springframework.data.relational.core.sql.render;
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.From;
import org.springframework.data.relational.core.sql.InlineQuery;
import org.springframework.data.relational.core.sql.Select;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.TestFrom;
/**
* Unit tests for the {@link FromClauseVisitor}.
*
* @author Jens Schauder
*/
public class FromClauseVisitorUnitTests {
StringBuilder renderResult = new StringBuilder();
FromClauseVisitor visitor = new FromClauseVisitor(new SimpleRenderContext(NamingStrategies.asIs()), renderResult::append);
@ParameterizedTest
@MethodSource
void testRendering(Fixture f) {
From from = f.from;
from.visit(visitor);
assertThat(renderResult.toString()).isEqualTo(f.renderResult);
}
static List<Fixture> testRendering() {
final Table tabOne = Table.create("tabOne");
final Table tabTwo = Table.create("tabTwo");
final Select selectOne = Select.builder().select(Column.create("oneId", tabOne)).from(tabOne).build();
final Select selectTwo = Select.builder().select(Column.create("twoId", tabTwo)).from(tabTwo).build();
return asList(
fixture("single table", new TestFrom(Table.create("one")), "one"),
fixture("single table with alias", new TestFrom(Table.aliased("one", "one_alias")), "one one_alias"),
fixture("multiple tables", new TestFrom(Table.create("one"),Table.create("two")), "one, two"),
fixture("multiple tables with alias", new TestFrom(Table.aliased("one", "one_alias"),Table.aliased("two", "two_alias")), "one one_alias, two two_alias"),
fixture("single inline query", new TestFrom(InlineQuery.create(selectOne, "ilAlias")), "(SELECT tabOne.oneId FROM tabOne) ilAlias"),
fixture("inline query with table", new TestFrom(InlineQuery.create(selectOne, "ilAlias"), tabTwo), "(SELECT tabOne.oneId FROM tabOne) ilAlias, tabTwo"),
fixture("table with inline query", new TestFrom(tabTwo,InlineQuery.create(selectOne, "ilAlias")), "tabTwo, (SELECT tabOne.oneId FROM tabOne) ilAlias"),
fixture("two inline queries", new TestFrom(InlineQuery.create(selectOne, "aliasOne"),InlineQuery.create(selectTwo, "aliasTwo")), "(SELECT tabOne.oneId FROM tabOne) aliasOne, (SELECT tabTwo.twoId FROM tabTwo) aliasTwo")
);
}
private static Fixture fixture(String comment, From from, String renderResult) {
Fixture fixture = new Fixture();
fixture.comment = comment;
fixture.from = from;
fixture.renderResult = renderResult;
return fixture;
}
static class Fixture {
String comment;
From from;
String renderResult;
@Override
public String toString() {
return comment;
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2021 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
*
* 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.
*/
package org.springframework.data.relational.core.sql.render;
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.InlineQuery;
import org.springframework.data.relational.core.sql.Join;
import org.springframework.data.relational.core.sql.Select;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.TestJoin;
import org.springframework.data.relational.core.sql.Visitor;
public class JoinVisitorTestsUnitTest {
final StringBuilder builder = new StringBuilder();
Visitor visitor = new JoinVisitor(new SimpleRenderContext(NamingStrategies.asIs()), builder::append);
@ParameterizedTest
@MethodSource
void renderJoins(Fixture f) {
Join join = f.join;
join.visit(visitor);
assertThat(builder.toString()).isEqualTo(f.renderResult);
}
static List<Fixture> renderJoins() {
Column colOne = Column.create("colOne", Table.create("tabOne"));
Table tabTwo = Table.create("tabTwo");
Column colTwo = Column.create("colTwo", tabTwo);
final Column renamed = colOne.as("renamed");
final Select select = Select.builder().select(renamed).from(colOne.getTable()).build();
final InlineQuery inlineQuery = InlineQuery.create(select, "inline");
return asList(
fixture("simple join", new TestJoin(Join.JoinType.JOIN, tabTwo, colOne.isEqualTo(colTwo)),
"JOIN tabTwo ON tabOne.colOne = tabTwo.colTwo"),
fixture("inlineQuery",
new TestJoin(Join.JoinType.JOIN, inlineQuery, colTwo.isEqualTo(inlineQuery.column("renamed"))),
"JOIN (SELECT tabOne.colOne AS renamed FROM tabOne) inline ON tabTwo.colTwo = inline.renamed"));
}
private static Fixture fixture(String comment, Join join, String renderResult) {
final Fixture fixture = new Fixture();
fixture.comment = comment;
fixture.join = join;
fixture.renderResult = renderResult;
return fixture;
}
static class Fixture {
String comment;
Join join;
String renderResult;
@Override
public String toString() {
return comment;
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2021 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
*
* 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.
*/
package org.springframework.data.relational.core.sql.render;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.Table;
/**
* Unit tests for the {@link NameRenderer}.
*
* @author Jens Schauder
*/
class NameRendererUnitTests {
RenderContext context = new SimpleRenderContext(NamingStrategies.asIs());
@Test // GH-1003
void rendersColumnWithoutTableName() {
Column column = Column.create("column", Table.create("table"));
CharSequence rendered = NameRenderer.render(context, column);
assertThat(rendered).isEqualTo("column");
}
@Test // GH-1003
void fullyQualifiedReference() {
Column column = Column.aliased("col", Table.aliased("table", "tab_alias"), "col_alias");
CharSequence rendered = NameRenderer.fullyQualifiedReference(context, column);
assertThat(rendered).isEqualTo("tab_alias.col_alias");
}
@Test // GH-1003
void fullyQualifiedUnaliasedReference() {
Column column = Column.aliased("col", Table.aliased("table", "tab_alias"), "col_alias");
CharSequence rendered = NameRenderer.fullyQualifiedUnaliasedReference(context, column);
assertThat(rendered).isEqualTo("tab_alias.col");
}
}

View File

@@ -18,7 +18,6 @@ package org.springframework.data.relational.core.sql.render;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.data.relational.core.dialect.PostgresDialect;
import org.springframework.data.relational.core.dialect.RenderContextFactory;
import org.springframework.data.relational.core.sql.*;
@@ -43,6 +42,18 @@ public class SelectRendererUnitTests {
assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT bar.foo FROM bar");
}
@Test
public void honorsNamingStrategy() {
Table bar = SQL.table("bar");
Column foo = bar.column("foo");
Select select = Select.builder().select(foo).from(bar).build();
assertThat(SqlRenderer.create(new SimpleRenderContext(NamingStrategies.toUpper())).render(select))
.isEqualTo("SELECT BAR.FOO FROM BAR");
}
@Test // DATAJDBC-309
public void shouldRenderAliasedColumnAndFrom() {
@@ -164,6 +175,55 @@ public class SelectRendererUnitTests {
+ "JOIN tenant tenant_base ON tenant_base.tenant_id = department.tenant");
}
@Test // GH-1003
public void shouldRenderJoinWithInlineQuery() {
Table employee = SQL.table("employee");
Table department = SQL.table("department");
Select innerSelect = Select.builder()
.select(employee.column("id"), employee.column("department_Id"), employee.column("name")).from(employee)
.build();
final InlineQuery one = InlineQuery.create(innerSelect, "one");
Select select = Select.builder().select(one.column("id"), department.column("name")).from(department) //
.join(one).on(one.column("department_id")).equals(department.column("id")) //
.build();
final String sql = SqlRenderer.toString(select);
assertThat(sql).isEqualTo("SELECT one.id, department.name FROM department " //
+ "JOIN (SELECT employee.id, employee.department_Id, employee.name FROM employee) one " //
+ "ON one.department_id = department.id");
}
@Test // GH-1003
public void shouldRenderJoinWithTwoInlineQueries() {
Table employee = SQL.table("employee");
Table department = SQL.table("department");
Select innerSelectOne = Select.builder()
.select(employee.column("id"), employee.column("department_Id"), employee.column("name")).from(employee)
.build();
Select innerSelectTwo = Select.builder().select(department.column("id"), department.column("name")).from(department)
.build();
final InlineQuery one = InlineQuery.create(innerSelectOne, "one");
final InlineQuery two = InlineQuery.create(innerSelectTwo, "two");
Select select = Select.builder().select(one.column("id"), two.column("name")).from(one) //
.join(two).on(two.column("department_id")).equals(one.column("id")) //
.build();
final String sql = SqlRenderer.toString(select);
assertThat(sql).isEqualTo("SELECT one.id, two.name FROM (" //
+ "SELECT employee.id, employee.department_Id, employee.name FROM employee) one " //
+ "JOIN (SELECT department.id, department.name FROM department) two " //
+ "ON two.department_id = one.id");
}
@Test // DATAJDBC-309
public void shouldRenderOrderByName() {

View File

@@ -0,0 +1,230 @@
/*
* Copyright 2021 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
*
* 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.
*/
package org.springframework.data.relational.core.sql.render;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.relational.core.sql.render.DelegatingVisitor.Delegation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.springframework.data.relational.core.sql.AbstractTestSegment;
import org.springframework.data.relational.core.sql.Segment;
import org.springframework.data.relational.core.sql.Visitable;
/**
* Unit tests for {@link org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor}.
*
* @author Jens Schauder
*/
class TypedSubtreeVisitorUnitTests {
List<String> events = new ArrayList<>();
@Test // GH-1003
void enterAndLeavesSingleSegment() {
final TypedSubtreeVisitor<TestSegment> visitor = new LoggingTypedSubtreeVisitor();
final TestSegment root = new TestSegment("root");
root.visit(visitor);
assertThat(events).containsExactly("enter matched root", "leave matched root");
}
@Test // GH-1003
void enterAndLeavesChainOfMatchingSegmentsAsNested() {
final TypedSubtreeVisitor<TestSegment> visitor = new LoggingTypedSubtreeVisitor();
final TestSegment root = new TestSegment("root", new TestSegment("level 1", new TestSegment("level 2")));
root.visit(visitor);
assertThat(events).containsExactly("enter matched root", "enter nested level 1", "enter nested level 2",
"leave nested level 2", "leave nested level 1", "leave matched root");
}
@Test // GH-1003
void enterAndLeavesMatchingChildrenAsNested() {
final TypedSubtreeVisitor<TestSegment> visitor = new LoggingTypedSubtreeVisitor();
final TestSegment root = new TestSegment("root", new TestSegment("child 1"), new TestSegment("child 2"));
root.visit(visitor);
assertThat(events).containsExactly("enter matched root", "enter nested child 1", "leave nested child 1",
"enter nested child 2", "leave nested child 2", "leave matched root");
}
@Test // GH-1003
void enterAndLeavesChainOfOtherSegmentsAsNested() {
final TypedSubtreeVisitor<TestSegment> visitor = new LoggingTypedSubtreeVisitor();
final TestSegment root = new TestSegment("root", new OtherSegment("level 1", new OtherSegment("level 2")));
root.visit(visitor);
assertThat(events).containsExactly("enter matched root", "enter nested level 1", "enter nested level 2",
"leave nested level 2", "leave nested level 1", "leave matched root");
}
@Test // GH-1003
void enterAndLeavesOtherChildrenAsNested() {
final TypedSubtreeVisitor<TestSegment> visitor = new LoggingTypedSubtreeVisitor();
final TestSegment root = new TestSegment("root", new OtherSegment("child 1"), new OtherSegment("child 2"));
root.visit(visitor);
assertThat(events).containsExactly("enter matched root", "enter nested child 1", "leave nested child 1",
"enter nested child 2", "leave nested child 2", "leave matched root");
}
@Test // GH-1003
void visitorIsReentrant() {
final LoggingTypedSubtreeVisitor visitor = new LoggingTypedSubtreeVisitor();
final TestSegment root1 = new TestSegment("root 1");
final TestSegment root2 = new TestSegment("root 2");
root1.visit(visitor);
root2.visit(visitor);
assertThat(events).containsExactly("enter matched root 1", "leave matched root 1", "enter matched root 2",
"leave matched root 2");
}
@Test // GH-1003
void delegateToOtherVisitorOnEnterMatchedRevisitsTheSegment() {
final LoggingTypedSubtreeVisitor first = new LoggingTypedSubtreeVisitor("first ");
final LoggingTypedSubtreeVisitor second = new LoggingTypedSubtreeVisitor("second ");
first.enterMatched(s -> delegateTo(second));
final TestSegment root = new TestSegment("root", new TestSegment("child 1"), new TestSegment("child 2"));
root.visit(first);
assertThat(events).containsExactly("first enter matched root", "second enter matched root",
"second enter nested child 1", "second leave nested child 1", "second enter nested child 2",
"second leave nested child 2", "second leave matched root", "first leave matched root");
}
@Test // GH-1003
void delegateToOtherVisitorOnEnterNestedRevisitsTheNestedSegment() {
final LoggingTypedSubtreeVisitor first = new LoggingTypedSubtreeVisitor("first ");
final LoggingTypedSubtreeVisitor second = new LoggingTypedSubtreeVisitor("second ");
first.enterNested(
s -> ((TestSegment) s).name.equals("child 2") ? delegateTo(second) : DelegatingVisitor.Delegation.retain());
final TestSegment root = new TestSegment("root", new TestSegment("child 1"), new TestSegment("child 2"),
new TestSegment("child 3"));
root.visit(first);
assertThat(events).containsExactly("first enter matched root", "first enter nested child 1",
"first leave nested child 1", "first enter nested child 2", "second enter matched child 2",
"second leave matched child 2", "first leave nested child 2", "first enter nested child 3",
"first leave nested child 3", "first leave matched root");
}
static class TestSegment extends AbstractTestSegment {
private final String name;
TestSegment(String name, Segment... children) {
super(children);
this.name = name;
}
@Override
public String toString() {
return name;
}
}
static class OtherSegment extends AbstractTestSegment {
private final String name;
public OtherSegment(String name, Segment... children) {
super(children);
this.name = name;
}
@Override
public String toString() {
return name;
}
}
class LoggingTypedSubtreeVisitor extends TypedSubtreeVisitor<TestSegment> {
final String prefix;
Function<TestSegment, Delegation> enterMatchedDelegation;
Function<Visitable, Delegation> enterNestedDelegation;
LoggingTypedSubtreeVisitor(String prefix) {
this.prefix = prefix;
}
LoggingTypedSubtreeVisitor() {
this("");
}
@Override
Delegation enterMatched(TestSegment segment) {
events.add(prefix + "enter matched " + segment);
final Delegation delegation = super.enterMatched(segment);
return enterMatchedDelegation == null ? delegation : enterMatchedDelegation.apply(segment);
}
void enterMatched(Function<TestSegment, Delegation> delegation) {
enterMatchedDelegation = delegation;
}
@Override
Delegation leaveMatched(TestSegment segment) {
events.add(prefix + "leave matched " + segment);
return super.leaveMatched(segment);
}
@Override
Delegation enterNested(Visitable segment) {
events.add(prefix + "enter nested " + segment);
return enterNestedDelegation == null ? super.enterNested(segment) : enterNestedDelegation.apply(segment);
}
void enterNested(Function<Visitable, Delegation> delegation) {
enterNestedDelegation = delegation;
}
@Override
Delegation leaveNested(Visitable segment) {
events.add(prefix + "leave nested " + segment);
return super.leaveNested(segment);
}
}
}