From 86c1bc4d06fafe504645b7441711b1d90903275f Mon Sep 17 00:00:00 2001 From: Oleg Babichev Date: Mon, 27 May 2024 08:56:52 +0200 Subject: [PATCH] feat: EXPOSED-295 Support subqueries with preceding LATERAL --- exposed-core/api/exposed-core.api | 42 +++++++------- .../kotlin/org/jetbrains/exposed/sql/Alias.kt | 23 ++++++-- .../kotlin/org/jetbrains/exposed/sql/Table.kt | 55 +++++++++++++------ .../exposed/sql/tests/DatabaseTestsBase.kt | 6 +- .../exposed/sql/tests/shared/dml/JoinTests.kt | 38 +++++++++++++ 5 files changed, 115 insertions(+), 49 deletions(-) diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index 745fbc89b1..41862465bb 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -151,10 +151,10 @@ public final class org/jetbrains/exposed/sql/AliasKt { public static final fun alias (Lorg/jetbrains/exposed/sql/Expression;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/ExpressionAlias; public static final fun alias (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Alias; public static final fun getLastQueryAlias (Lorg/jetbrains/exposed/sql/Join;)Lorg/jetbrains/exposed/sql/QueryAlias; - public static final fun joinQuery (Lorg/jetbrains/exposed/sql/Join;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;Lkotlin/jvm/functions/Function0;)Lorg/jetbrains/exposed/sql/Join; - public static final fun joinQuery (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;Lkotlin/jvm/functions/Function0;)Lorg/jetbrains/exposed/sql/Join; - public static synthetic fun joinQuery$default (Lorg/jetbrains/exposed/sql/Join;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; - public static synthetic fun joinQuery$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun joinQuery (Lorg/jetbrains/exposed/sql/Join;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;ZLkotlin/jvm/functions/Function0;)Lorg/jetbrains/exposed/sql/Join; + public static final fun joinQuery (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;ZLkotlin/jvm/functions/Function0;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun joinQuery$default (Lorg/jetbrains/exposed/sql/Join;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun joinQuery$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function2;Lorg/jetbrains/exposed/sql/JoinType;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; public static final fun wrapAsExpression (Lorg/jetbrains/exposed/sql/AbstractQuery;)Lorg/jetbrains/exposed/sql/Expression; } @@ -447,8 +447,8 @@ public abstract class org/jetbrains/exposed/sql/ColumnSet : org/jetbrains/expose public fun getRealFields ()Ljava/util/List; public fun getSource ()Lorg/jetbrains/exposed/sql/ColumnSet; public abstract fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; - public abstract fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; - public static synthetic fun join$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public abstract fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;ZLkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun join$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; public abstract fun leftJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; public abstract fun rightJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; public final fun select (Ljava/util/List;)Lorg/jetbrains/exposed/sql/Query; @@ -1336,8 +1336,8 @@ public final class org/jetbrains/exposed/sql/IterableExKt { public final class org/jetbrains/exposed/sql/Join : org/jetbrains/exposed/sql/ColumnSet { public fun (Lorg/jetbrains/exposed/sql/ColumnSet;)V - public fun (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;ZLkotlin/jvm/functions/Function1;)V + public synthetic fun (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun alreadyInJoin (Lorg/jetbrains/exposed/sql/Table;)Z public fun crossJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; public fun describe (Lorg/jetbrains/exposed/sql/Transaction;Lorg/jetbrains/exposed/sql/QueryBuilder;)V @@ -1345,7 +1345,7 @@ public final class org/jetbrains/exposed/sql/Join : org/jetbrains/exposed/sql/Co public fun getColumns ()Ljava/util/List; public final fun getTable ()Lorg/jetbrains/exposed/sql/ColumnSet; public fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; - public fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;ZLkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; public fun leftJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; public fun rightJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; } @@ -1798,7 +1798,7 @@ public final class org/jetbrains/exposed/sql/QueryAlias : org/jetbrains/exposed/ public fun getFields ()Ljava/util/List; public final fun getQuery ()Lorg/jetbrains/exposed/sql/AbstractQuery; public fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; - public fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;ZLkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; public fun leftJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; public fun rightJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; } @@ -2368,7 +2368,7 @@ public class org/jetbrains/exposed/sql/Table : org/jetbrains/exposed/sql/ColumnS public static synthetic fun index$default (Lorg/jetbrains/exposed/sql/Table;Z[Lorg/jetbrains/exposed/sql/Column;ILjava/lang/Object;)V public fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; public final fun integer (Ljava/lang/String;)Lorg/jetbrains/exposed/sql/Column; - public fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public fun join (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/JoinType;Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;ZLkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; public final fun largeText (Ljava/lang/String;Ljava/lang/String;Z)Lorg/jetbrains/exposed/sql/Column; public static synthetic fun largeText$default (Lorg/jetbrains/exposed/sql/Table;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Column; public fun leftJoin (Lorg/jetbrains/exposed/sql/ColumnSet;)Lorg/jetbrains/exposed/sql/Join; @@ -2433,16 +2433,16 @@ public final class org/jetbrains/exposed/sql/Table$PrimaryKey { } public final class org/jetbrains/exposed/sql/TableKt { - public static final fun crossJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; - public static synthetic fun crossJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; - public static final fun fullJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; - public static synthetic fun fullJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; - public static final fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; - public static synthetic fun innerJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; - public static final fun leftJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; - public static synthetic fun leftJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; - public static final fun rightJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; - public static synthetic fun rightJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun crossJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun crossJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun fullJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun fullJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun innerJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun innerJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun leftJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun leftJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; + public static final fun rightJoin (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Join; + public static synthetic fun rightJoin$default (Lorg/jetbrains/exposed/sql/ColumnSet;Lorg/jetbrains/exposed/sql/ColumnSet;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/Join; public static final fun targetTables (Lorg/jetbrains/exposed/sql/ColumnSet;)Ljava/util/List; } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt index c59d718602..83792e33b8 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Alias.kt @@ -102,9 +102,10 @@ class QueryAlias(val query: AbstractQuery<*>, val alias: String) : ColumnSet() { joinType: JoinType, onColumn: Expression<*>?, otherColumn: Expression<*>?, - additionalConstraint: (SqlExpressionBuilder.() -> Op)? + lateral: Boolean, + additionalConstraint: (SqlExpressionBuilder.() -> Op)?, ): Join = - Join(this, otherTable, joinType, onColumn, otherColumn, additionalConstraint) + Join(this, otherTable, joinType, onColumn, otherColumn, lateral, additionalConstraint) override infix fun innerJoin(otherTable: ColumnSet): Join = Join(this, otherTable, JoinType.INNER) @@ -157,9 +158,14 @@ fun Expression.alias(alias: String) = ExpressionAlias(this, alias) * @param joinPart The query to join with. * @sample org.jetbrains.exposed.sql.tests.shared.AliasesTests.testJoinSubQuery02 */ -fun Join.joinQuery(on: (SqlExpressionBuilder.(QueryAlias) -> Op), joinType: JoinType = JoinType.INNER, joinPart: () -> AbstractQuery<*>): Join { +fun Join.joinQuery( + on: (SqlExpressionBuilder.(QueryAlias) -> Op)? = null, + joinType: JoinType = JoinType.INNER, + lateral: Boolean = false, + joinPart: () -> AbstractQuery<*> +): Join { val qAlias = joinPart().alias("q${joinParts.count { it.joinPart is QueryAlias }}") - return join(qAlias, joinType, additionalConstraint = { on(qAlias) }) + return join(qAlias, joinType, lateral = lateral, additionalConstraint = on?.let { { it(qAlias) } }) } /** @@ -169,8 +175,13 @@ fun Join.joinQuery(on: (SqlExpressionBuilder.(QueryAlias) -> Op), joinT * @param joinType The `JOIN` clause type used to combine rows. Defaults to [JoinType.INNER]. * @param joinPart The query to join with. */ -fun Table.joinQuery(on: (SqlExpressionBuilder.(QueryAlias) -> Op), joinType: JoinType = JoinType.INNER, joinPart: () -> AbstractQuery<*>) = - Join(this).joinQuery(on, joinType, joinPart) +fun Table.joinQuery( + on: (SqlExpressionBuilder.(QueryAlias) -> Op)? = null, + joinType: JoinType = JoinType.INNER, + lateral: Boolean = false, + joinPart: () -> AbstractQuery<*> +) = + Join(this).joinQuery(on, joinType, lateral, joinPart) /** * Returns the most recent [QueryAlias] instance used to create this join relation, or `null` if a query was not joined. diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt index 444c3c23c3..dfd0424b84 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Table.kt @@ -76,7 +76,8 @@ abstract class ColumnSet : FieldSet { * @param onColumn The column from a current [ColumnSet], may be skipped then [additionalConstraint] will be used. * @param otherColumn The column from an [otherTable], may be skipped then [additionalConstraint] will be used. * @param additionalConstraint The condition to join which will be placed in ON part of SQL query. - * + * @param lateral Set to true to enable a lateral join, allowing the subquery on the right side + * to access columns from preceding tables in the FROM clause. * @throws IllegalStateException If join could not be prepared. See exception message for more details. */ abstract fun join( @@ -84,7 +85,8 @@ abstract class ColumnSet : FieldSet { joinType: JoinType, onColumn: Expression<*>? = null, otherColumn: Expression<*>? = null, - additionalConstraint: (SqlExpressionBuilder.() -> Op)? = null + lateral: Boolean = false, + additionalConstraint: (SqlExpressionBuilder.() -> Op)? = null, ): Join /** Creates an inner join relation with [otherTable]. */ @@ -147,8 +149,9 @@ fun C1.innerJoin( otherTable: C2, onColumn: (C1.() -> Expression<*>)? = null, otherColumn: (C2.() -> Expression<*>)? = null, + lateral: Boolean = false, additionalConstraint: (SqlExpressionBuilder.() -> Op)? = null, -): Join = join(otherTable, JoinType.INNER, onColumn?.invoke(this), otherColumn?.invoke(otherTable), additionalConstraint) +): Join = join(otherTable, JoinType.INNER, onColumn?.invoke(this), otherColumn?.invoke(otherTable), lateral, additionalConstraint) /** * Creates a left outer join relation with [otherTable] using [onColumn] and [otherColumn] equality @@ -160,8 +163,9 @@ fun C1.leftJoin( otherTable: C2, onColumn: (C1.() -> Expression<*>)? = null, otherColumn: (C2.() -> Expression<*>)? = null, + lateral: Boolean = false, additionalConstraint: (SqlExpressionBuilder.() -> Op)? = null, -): Join = join(otherTable, JoinType.LEFT, onColumn?.invoke(this), otherColumn?.invoke(otherTable), additionalConstraint) +): Join = join(otherTable, JoinType.LEFT, onColumn?.invoke(this), otherColumn?.invoke(otherTable), lateral, additionalConstraint) /** * Creates a right outer join relation with [otherTable] using [onColumn] and [otherColumn] equality @@ -173,8 +177,9 @@ fun C1.rightJoin( otherTable: C2, onColumn: (C1.() -> Expression<*>)? = null, otherColumn: (C2.() -> Expression<*>)? = null, + lateral: Boolean = false, additionalConstraint: (SqlExpressionBuilder.() -> Op)? = null, -): Join = join(otherTable, JoinType.RIGHT, onColumn?.invoke(this), otherColumn?.invoke(otherTable), additionalConstraint) +): Join = join(otherTable, JoinType.RIGHT, onColumn?.invoke(this), otherColumn?.invoke(otherTable), lateral, additionalConstraint) /** * Creates a full outer join relation with [otherTable] using [onColumn] and [otherColumn] equality @@ -186,8 +191,9 @@ fun C1.fullJoin( otherTable: C2, onColumn: (C1.() -> Expression<*>)? = null, otherColumn: (C2.() -> Expression<*>)? = null, + lateral: Boolean = false, additionalConstraint: (SqlExpressionBuilder.() -> Op)? = null, -): Join = join(otherTable, JoinType.FULL, onColumn?.invoke(this), otherColumn?.invoke(otherTable), additionalConstraint) +): Join = join(otherTable, JoinType.FULL, onColumn?.invoke(this), otherColumn?.invoke(otherTable), lateral, additionalConstraint) /** * Creates a cross join relation with [otherTable] using [onColumn] and [otherColumn] equality @@ -199,8 +205,9 @@ fun C1.crossJoin( otherTable: C2, onColumn: (C1.() -> Expression<*>)? = null, otherColumn: (C2.() -> Expression<*>)? = null, + lateral: Boolean = false, additionalConstraint: (SqlExpressionBuilder.() -> Op)? = null, -): Join = join(otherTable, JoinType.CROSS, onColumn?.invoke(this), otherColumn?.invoke(otherTable), additionalConstraint) +): Join = join(otherTable, JoinType.CROSS, onColumn?.invoke(this), otherColumn?.invoke(otherTable), lateral, additionalConstraint) /** * Represents a subset of [fields] from a given [source]. @@ -247,11 +254,12 @@ class Join( joinType: JoinType = JoinType.INNER, onColumn: Expression<*>? = null, otherColumn: Expression<*>? = null, - additionalConstraint: (SqlExpressionBuilder.() -> Op)? = null + lateral: Boolean = false, + additionalConstraint: (SqlExpressionBuilder.() -> Op)? = null, ) : this(table) { val newJoin = when { onColumn != null && otherColumn != null -> { - join(otherTable, joinType, onColumn, otherColumn, additionalConstraint) + join(otherTable, joinType, onColumn, otherColumn, lateral, additionalConstraint) } onColumn != null || otherColumn != null -> { @@ -259,7 +267,7 @@ class Join( } additionalConstraint != null -> { - join(otherTable, joinType, emptyList(), additionalConstraint) + join(otherTable, joinType, emptyList(), additionalConstraint, lateral) } else -> { @@ -281,6 +289,7 @@ class Join( joinType: JoinType, onColumn: Expression<*>?, otherColumn: Expression<*>?, + lateral: Boolean, additionalConstraint: (SqlExpressionBuilder.() -> Op)? ): Join { val cond = if (onColumn != null && otherColumn != null) { @@ -288,7 +297,7 @@ class Join( } else { emptyList() } - return join(otherTable, joinType, cond, additionalConstraint) + return join(otherTable, joinType, cond, additionalConstraint, lateral) } override infix fun innerJoin(otherTable: ColumnSet): Join = implicitJoin(otherTable, JoinType.INNER) @@ -327,16 +336,20 @@ class Join( } } + @Suppress("MemberNameEqualsClassName") + private fun join(part: JoinPart): Join = Join(table).also { + it.joinParts.addAll(this.joinParts) + it.joinParts.add(part) + } + @Suppress("MemberNameEqualsClassName") private fun join( otherTable: ColumnSet, joinType: JoinType, cond: List, - additionalConstraint: (SqlExpressionBuilder.() -> Op)? - ): Join = Join(table).also { - it.joinParts.addAll(this.joinParts) - it.joinParts.add(JoinPart(joinType, otherTable, cond, additionalConstraint)) - } + additionalConstraint: (SqlExpressionBuilder.() -> Op)?, + lateral: Boolean = false + ): Join = join(JoinPart(joinType, otherTable, cond, lateral, additionalConstraint)) private fun findKeys(a: ColumnSet, b: ColumnSet): List, List>>>? = a.columns .map { a_pk -> a_pk to b.columns.filter { it.referee == a_pk } } @@ -354,6 +367,8 @@ class Join( val joinPart: ColumnSet, /** The [JoinCondition] expressions used to match rows from two joined tables. */ val conditions: List, + /** Indicates whether the LATERAL keyword should be included in the JOIN operation. */ + val lateral: Boolean = false, /** The conditions used to join tables, placed in the `ON` clause. */ val additionalConstraint: (SqlExpressionBuilder.() -> Op)? = null ) { @@ -366,6 +381,11 @@ class Join( /** Appends the SQL representation of this join component to the specified [QueryBuilder]. */ fun describe(transaction: Transaction, builder: QueryBuilder) = with(builder) { append(" $joinType JOIN ") + + if (lateral) { + append("LATERAL ") + } + val isJoin = joinPart is Join if (isJoin) { append("(") @@ -499,8 +519,9 @@ open class Table(name: String = "") : ColumnSet(), DdlAware { joinType: JoinType, onColumn: Expression<*>?, otherColumn: Expression<*>?, + lateral: Boolean, additionalConstraint: (SqlExpressionBuilder.() -> Op)? - ): Join = Join(this, otherTable, joinType, onColumn, otherColumn, additionalConstraint) + ): Join = Join(this, otherTable, joinType, onColumn, otherColumn, lateral, additionalConstraint) override infix fun innerJoin(otherTable: ColumnSet): Join = Join(this, otherTable, JoinType.INNER) diff --git a/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/DatabaseTestsBase.kt b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/DatabaseTestsBase.kt index 49d3c7e0c9..d27e2d4847 100644 --- a/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/DatabaseTestsBase.kt +++ b/exposed-tests/src/main/kotlin/org/jetbrains/exposed/sql/tests/DatabaseTestsBase.kt @@ -1,10 +1,6 @@ package org.jetbrains.exposed.sql.tests -import org.jetbrains.exposed.sql.Key -import org.jetbrains.exposed.sql.Schema -import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.StatementInterceptor import org.jetbrains.exposed.sql.transactions.inTopLevelTransaction import org.jetbrains.exposed.sql.transactions.nullableTransactionScope diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/JoinTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/JoinTests.kt index 9718d34c74..06cd0ccfd4 100644 --- a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/JoinTests.kt +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/JoinTests.kt @@ -4,6 +4,8 @@ import nl.altindag.log.LogCaptor import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.tests.DatabaseTestsBase +import org.jetbrains.exposed.sql.tests.TestDB +import org.jetbrains.exposed.sql.tests.shared.assertEqualLists import org.jetbrains.exposed.sql.tests.shared.assertEquals import org.jetbrains.exposed.sql.tests.shared.expectException import org.junit.Test @@ -220,4 +222,40 @@ class JoinTests : DatabaseTestsBase() { assertTrue(logCaptor.errorLogs.isEmpty()) } } + + @Test + fun testLateralJoin() { + // lateral join is also supported by MySql8 database, but at the current moment there is no related configuration + val lateralJoinSupportedDb = listOf(TestDB.POSTGRESQL, TestDB.POSTGRESQLNG, TestDB.ORACLE) + + val tester1 = object : IntIdTable() { + val value = integer("value") + } + val tester2 = object : IntIdTable() { + val tester1 = reference("tester1", tester1.id) + val value = integer("value") + } + + withTables(TestDB.entries - lateralJoinSupportedDb, tester1, tester2) { + val id = tester1.insertAndGetId { it[value] = 20 } + + listOf(10, 30).forEach { value -> + tester2.insert { + it[tester2.value] = value + it[tester2.tester1] = id + } + } + + val query = tester1.joinQuery( + joinType = JoinType.CROSS, + lateral = true + ) { + tester2.selectAll().where { tester2.value greater tester1.value }.limit(1) + } + + val subqueryAlias = query.lastQueryAlias ?: error("Alias must exist!") + + assertEqualLists(listOf(30), query.selectAll().map { it[subqueryAlias[tester2.value]] }) + } + } }