From bbe22e032bce6dd00e852664e6b633e5c62a78fe Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 22 Jul 2021 14:11:49 +0200 Subject: [PATCH 1/3] 1003-join-subselect - Prepare branch --- pom.xml | 2 +- spring-data-jdbc-distribution/pom.xml | 2 +- spring-data-jdbc/pom.xml | 4 ++-- spring-data-relational/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index eeaa0b9e93..557756261e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-1003-join-subselect-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 03d6a5c2a0..36970858d9 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-1003-join-subselect-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index af9ad0904e..40b19527d3 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 2.3.0-SNAPSHOT + 2.3.0-1003-join-subselect-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-1003-join-subselect-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 4e42a006ec..af62c76fcb 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 2.3.0-SNAPSHOT + 2.3.0-1003-join-subselect-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-1003-join-subselect-SNAPSHOT From d8574a86546410d9d7aebb43e4f83d395ca7e821 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Fri, 23 Jul 2021 16:48:51 +0200 Subject: [PATCH 2/3] Support for in line queries for tables. `InlineQuery` can be used whereever 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 --- .../jdbc/repository/query/QueryMapper.java | 2 +- .../core/dialect/PostgresDialect.java | 8 +- .../core/sql/AsteriskFromTable.java | 14 +- .../data/relational/core/sql/Column.java | 14 +- .../relational/core/sql/DefaultSelect.java | 2 +- .../core/sql/DefaultSelectBuilder.java | 22 +- .../data/relational/core/sql/From.java | 10 +- .../data/relational/core/sql/InlineQuery.java | 99 ++++++++ .../data/relational/core/sql/Join.java | 6 +- .../relational/core/sql/SelectBuilder.java | 30 +-- .../relational/core/sql/SelectValidator.java | 20 +- .../data/relational/core/sql/Table.java | 113 +-------- .../data/relational/core/sql/TableLike.java | 138 +++++++++++ .../core/sql/render/ColumnVisitor.java | 10 +- .../core/sql/render/ExpressionVisitor.java | 49 +++- .../core/sql/render/FromTableVisitor.java | 43 +++- .../core/sql/render/JoinVisitor.java | 15 +- .../core/sql/render/NameRenderer.java | 58 ++--- .../core/sql/render/NamingStrategies.java | 13 +- .../core/sql/render/RenderNamingStrategy.java | 16 +- .../core/sql/render/SelectListVisitor.java | 49 +--- .../core/sql/AbstractTestSegment.java | 27 ++ .../data/relational/core/sql/TestFrom.java | 28 +++ .../data/relational/core/sql/TestJoin.java | 27 ++ .../render/ExpressionVisitorUnitTests.java | 139 +++++++++++ .../render/FromClauseVisitorUnitTests.java | 92 +++++++ .../sql/render/JoinVisitorTestsUnitTest.java | 87 +++++++ .../sql/render/NameRendererUnitTests.java | 62 +++++ .../sql/render/SelectRendererUnitTests.java | 72 +++++- .../render/TypedSubtreeVisitorUnitTests.java | 230 ++++++++++++++++++ 30 files changed, 1200 insertions(+), 295 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/InlineQuery.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TableLike.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/AbstractTestSegment.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestFrom.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestJoin.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/FromClauseVisitorUnitTests.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/JoinVisitorTestsUnitTest.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/NameRendererUnitTests.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitorUnitTests.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java index 6f75dbb081..a9cd3f35e8 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java @@ -121,7 +121,7 @@ Expression getMappedObject(Expression expression, @Nullable RelationalPersistent 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)); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java index a8d95c517c..9495c2cd30 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java @@ -23,11 +23,11 @@ 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 @@ static class PostgresLockClause implements LockClause { @Override public String getLock(LockOptions lockOptions) { - List tables = lockOptions.getFrom().getTables(); + List tables = lockOptions.getFrom().getTables(); if (tables.isEmpty()) { return ""; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AsteriskFromTable.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AsteriskFromTable.java index 62f331137b..b2af696f39 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AsteriskFromTable.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AsteriskFromTable.java @@ -18,13 +18,7 @@ /** * {@link Segment} to select all columns from a {@link Table}. *

- * * Renders to: {@code - * -

- * .*} as in {@code SELECT - * -
- * .* FROM …}. + * Renders to: {@code
.*} as in {@code SELECT
.* FROM …}. * * @author Mark Paluch * @since 1.1 @@ -32,9 +26,9 @@ */ 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 static AsteriskFromTable create(Table table) { /** * @return the associated {@link Table}. */ - public Table getTable() { + public TableLike getTable() { return table; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java index f377f5af9e..29ce325d16 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Column.java @@ -30,9 +30,9 @@ 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 SqlIdentifier getReferenceName() { * {@link Table}. */ @Nullable - public Table getTable() { + public TableLike getTable() { return table; } @@ -370,12 +370,12 @@ static class AliasedColumn extends Column implements Aliased { 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; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java index 80a4e40c52..06f449d78e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java @@ -42,7 +42,7 @@ class DefaultSelect implements Select { private final List orderBy; private final @Nullable LockMode lockMode; - DefaultSelect(boolean distinct, List selectList, List
from, long limit, long offset, + DefaultSelect(boolean distinct, List selectList, List from, long limit, long offset, List joins, @Nullable Condition where, List orderBy, @Nullable LockMode lockMode) { this.distinct = distinct; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java index 61de3bc716..1e071744a1 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java @@ -38,7 +38,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn private boolean distinct = false; private List selectList = new ArrayList<>(); - private List
from = new ArrayList<>(); + private List from = new ArrayList<>(); private long limit = -1; private long offset = -1; private List joins = new ArrayList<>(); @@ -107,7 +107,7 @@ public SelectFromAndJoin from(String table) { * @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 @@ public SelectFromAndJoin from(Table table) { * @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 @@ public SelectFromAndJoin from(Table... tables) { * @see org.springframework.data.relational.core.sql.SelectBuilder.SelectAndFrom#from(java.util.Collection) */ @Override - public SelectFromAndJoin from(Collection tables) { + public SelectFromAndJoin from(Collection tables) { from.addAll(tables); return this; } @@ -248,7 +248,7 @@ public SelectOn join(String table) { * @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 @@ public SelectOn join(Table table) { * @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 @@ public Select build() { */ 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 @@ public SelectOn join(String table) { * @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 @@ public SelectOn join(Table table) { * @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); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/From.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/From.java index 6f87aab370..e41d6955d2 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/From.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/From.java @@ -29,20 +29,20 @@ */ public class From extends AbstractSegment { - private final List
tables; + private final List tables; - From(Table... tables) { + From(TableLike... tables) { this(Arrays.asList(tables)); } - From(List
tables) { + From(List tables) { - super(tables.toArray(new Table[] {})); + super(tables.toArray(new TableLike[] {})); this.tables = Collections.unmodifiableList(tables); } - public List
getTables() { + public List getTables() { return this.tables; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/InlineQuery.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/InlineQuery.java new file mode 100644 index 0000000000..88c083afd5 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/InlineQuery.java @@ -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. + *

+ * Renders to: {@code ( selects = new Stack<>(); private int selectFieldCount; - private Set

requiredBySelect = new HashSet<>(); - private Set
requiredByOrderBy = new HashSet<>(); + private Set requiredBySelect = new HashSet<>(); + private Set requiredByOrderBy = new HashSet<>(); - private Set
join = new HashSet<>(); + private Set join = new HashSet<>(); /** * Validates a {@link Select} statement. @@ -57,7 +57,7 @@ private void doValidate(Select select) { 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 @@ private void doValidate(Select select) { } } - 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 @@ public void enter(Visitable segment) { 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 @@ public void enter(Visitable segment) { 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); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java index 86707a6235..99023d5435 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java @@ -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}. *

* Renders to: {@code } or {@code AS }. @@ -31,7 +26,7 @@ * @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 Table as(SqlIdentifier alias) { return new AliasedTable(name, alias); } - /** - * Creates a new {@link Column} associated with this {@link Table}. - *

- * 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}. - *

- * 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}. - *

- * 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 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}. - *

- * 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 columns(SqlIdentifier... names) { - - Assert.notNull(names, "Names must not be null"); - - List 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}. - *

- * 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 columns(Collection names) { - - Assert.notNull(names, "Names must not be null"); - - List 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 - * -

- * .*}. - * - * @return the select all marker for this {@link Table}. - */ - public AsteriskFromTable asterisk() { - return new AsteriskFromTable(this); - } - /** * @return the table name. */ diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TableLike.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TableLike.java new file mode 100644 index 0000000000..ef5484e5ff --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TableLike.java @@ -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}. + *

+ * 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}. + *

+ * 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}. + *

+ * 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 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}. + *

+ * 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 columns(SqlIdentifier... names) { + + Assert.notNull(names, "Names must not be null"); + + List 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}. + *

+ * 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 columns(Collection names) { + + Assert.notNull(names, "Names must not be null"); + + List 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 + * +

+ * .*}. + * + * @return the select all marker for this {@link Table}. + */ + default AsteriskFromTable asterisk() { + return new AsteriskFromTable(this); + } + + SqlIdentifier getName(); + + SqlIdentifier getReferenceName(); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ColumnVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ColumnVisitor.java index 78da0bf13a..9f49d5e2db 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ColumnVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ColumnVisitor.java @@ -17,12 +17,14 @@ 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 + *
+ * .} or {@literal }. * * @author Mark Paluch * @since 1.1 @@ -65,8 +67,8 @@ Delegation leaveMatched(Column segment) { @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); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java index a568c23d47..0f890f2915 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java @@ -15,16 +15,17 @@ */ 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; import org.springframework.data.relational.core.sql.Expression; -import org.springframework.data.relational.core.sql.Literal; import org.springframework.data.relational.core.sql.Named; 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. @@ -38,12 +39,33 @@ class ExpressionVisitor extends TypedSubtreeVisitor 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; } /* @@ -71,7 +93,8 @@ Delegation enterMatched(Expression segment) { 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) { @@ -79,7 +102,10 @@ Delegation enterMatched(Expression segment) { } else { value = segment.toString(); } - } else if (segment instanceof Literal) { + } else if (segment instanceof AsteriskFromTable) { + value = NameRenderer.render(context, ((AsteriskFromTable) segment).getTable()) + ".*"; + } else { + // works for literals and just and possibly more value = segment.toString(); } @@ -94,6 +120,7 @@ Delegation enterMatched(Expression segment) { Delegation enterNested(Visitable segment) { if (segment instanceof Condition) { + ConditionVisitor visitor = new ConditionVisitor(context); partRenderer = visitor; return Delegation.delegateTo(visitor); @@ -125,4 +152,20 @@ Delegation leaveMatched(Expression segment) { 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 }. + */ + USE + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromTableVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromTableVisitor.java index 2fd80d8278..89bffd3560 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromTableVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/FromTableVisitor.java @@ -17,20 +17,28 @@ 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
{ +class FromTableVisitor extends TypedSubtreeVisitor { 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
{ * @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 @@ Delegation enterMatched(Table segment) { parent.onRendered(builder); - return super.enterMatched(segment); + return super.leaveMatched(segment); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java index c5a560ee6f..91273258a3 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java @@ -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 @@ */ class JoinVisitor extends TypedSubtreeVisitor { - 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 @@ Delegation enterMatched(Join segment) { @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 @@ Delegation leaveNested(Visitable segment) { */ @Override Delegation leaveMatched(Join segment) { + parent.onRendered(joinClause); return super.leaveMatched(segment); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NameRenderer.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NameRenderer.java index 0eedad6f28..831346b6a8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NameRenderer.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NameRenderer.java @@ -21,34 +21,28 @@ 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 @@ static CharSequence render(RenderContext context, Column column) { /** * 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 @@ static CharSequence render(RenderContext context, Named named) { /** * 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 @@ static CharSequence render(RenderContext context, Aliased aliased) { /** * 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 @@ static CharSequence reference(RenderContext context, Column column) { /** * 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 @@ static CharSequence fullyQualifiedReference(RenderContext context, Column column 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()); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NamingStrategies.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NamingStrategies.java index 47c2f65563..e0f51ddb50 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NamingStrategies.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/NamingStrategies.java @@ -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 static RenderNamingStrategy toLower(Locale locale) { } enum AsIs implements RenderNamingStrategy { - INSTANCE; + INSTANCE } static class DelegatingRenderNamingStrategy implements RenderNamingStrategy { @@ -135,12 +136,12 @@ public SqlIdentifier getReferenceName(Column column) { } @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); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderNamingStrategy.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderNamingStrategy.java index aac84a38b0..01f2661066 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderNamingStrategy.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderNamingStrategy.java @@ -20,6 +20,7 @@ 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 @@ * Naming strategy for SQL rendering. * * @author Mark Paluch + * @author Jens Schauder * @see NamingStrategies * @since 1.1 */ @@ -55,24 +57,24 @@ default SqlIdentifier getReferenceName(Column column) { } /** - * 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(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java index 5e399911a9..8c4fd8c811 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java @@ -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 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 @@ Delegation enterNested(Visitable segment) { 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 @@ Delegation leaveMatched(SelectList segment) { @Override Delegation leaveNested(Visitable segment) { - if (segment instanceof Table) { - builder.append(NameRenderer.reference(context, (Table) segment)).append('.'); - } - - if (segment instanceof SimpleFunction) { + if (segment instanceof Expression) { - 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("*"); + builder.append(expressionVisitor.getRenderedPart()); 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 @@ Delegation leaveNested(Visitable segment) { public CharSequence getRenderedPart() { return builder; } + + } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/AbstractTestSegment.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/AbstractTestSegment.java new file mode 100644 index 0000000000..98991efde1 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/AbstractTestSegment.java @@ -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); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestFrom.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestFrom.java new file mode 100644 index 0000000000..f168b5b1b5 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestFrom.java @@ -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); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestJoin.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestJoin.java new file mode 100644 index 0000000000..ff4cd69194 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestJoin.java @@ -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); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java new file mode 100644 index 0000000000..9808aed290 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java @@ -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 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; + } + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/FromClauseVisitorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/FromClauseVisitorUnitTests.java new file mode 100644 index 0000000000..a0cb0d1d7e --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/FromClauseVisitorUnitTests.java @@ -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 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; + } + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/JoinVisitorTestsUnitTest.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/JoinVisitorTestsUnitTest.java new file mode 100644 index 0000000000..382608ea9f --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/JoinVisitorTestsUnitTest.java @@ -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 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; + } + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/NameRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/NameRendererUnitTests.java new file mode 100644 index 0000000000..c87d795310 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/NameRendererUnitTests.java @@ -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"); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java index abf4a88ecc..90a802fda3 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java @@ -18,18 +18,9 @@ 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.Column; -import org.springframework.data.relational.core.sql.Conditions; -import org.springframework.data.relational.core.sql.Expressions; -import org.springframework.data.relational.core.sql.Functions; -import org.springframework.data.relational.core.sql.OrderByField; -import org.springframework.data.relational.core.sql.SQL; -import org.springframework.data.relational.core.sql.Select; -import org.springframework.data.relational.core.sql.SqlIdentifier; -import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.*; import org.springframework.util.StringUtils; /** @@ -51,6 +42,18 @@ public void shouldRenderSingleColumn() { 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() { @@ -172,6 +175,55 @@ public void shouldRenderMultipleJoinWithAnd() { + "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() { diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitorUnitTests.java new file mode 100644 index 0000000000..d20dc2a20a --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitorUnitTests.java @@ -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 events = new ArrayList<>(); + + @Test // GH-1003 + void enterAndLeavesSingleSegment() { + + final TypedSubtreeVisitor 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 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 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 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 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 { + + final String prefix; + Function enterMatchedDelegation; + Function 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 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 delegation) { + enterNestedDelegation = delegation; + } + + @Override + Delegation leaveNested(Visitable segment) { + + events.add(prefix + "leave nested " + segment); + return super.leaveNested(segment); + } + + } +} From 334fe0e5e129e057633f2a7976ad4007dc4c7d0f Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Fri, 27 Aug 2021 09:31:57 +0200 Subject: [PATCH 3/3] Polishing. See #1003 Original pull request #1018 --- .../org/springframework/data/relational/core/sql/Table.java | 2 +- .../org/springframework/data/relational/core/sql/TestFrom.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java index 99023d5435..640066796a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Table.java @@ -26,7 +26,7 @@ * @author Mark Paluch * @since 1.1 */ -public class Table extends AbstractSegment implements TableLike { +public class Table extends AbstractSegment implements TableLike { private final SqlIdentifier name; diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestFrom.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestFrom.java index f168b5b1b5..d30547ec49 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestFrom.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TestFrom.java @@ -20,7 +20,7 @@ * * @author Jens Schauder */ -public class TestFrom extends From{ +public class TestFrom extends From { public TestFrom(TableLike... tables) { super(tables);