diff --git a/core/src/main/kotlin/ch/ergon/dope/resolvable/clause/ISelectClause.kt b/core/src/main/kotlin/ch/ergon/dope/resolvable/clause/ISelectClause.kt index 9b5494af..0d902a10 100644 --- a/core/src/main/kotlin/ch/ergon/dope/resolvable/clause/ISelectClause.kt +++ b/core/src/main/kotlin/ch/ergon/dope/resolvable/clause/ISelectClause.kt @@ -67,15 +67,17 @@ interface ISelectFromClause : ISelectUseKeysClause { interface ISelectJoinClause : ISelectFromClause { fun join(bucket: Bucket, onCondition: TypeExpression) = StandardJoinClause(bucket, onCondition, this) fun join(bucket: Bucket, onKeys: Field) = StandardJoinClause(bucket, onKeys, this) + fun join(bucket: Bucket, onKey: Field, forBucket: Bucket) = StandardJoinClause(bucket, onKey, forBucket, this) fun innerJoin(bucket: Bucket, onCondition: TypeExpression) = InnerJoinClause(bucket, onCondition, this) fun innerJoin(bucket: Bucket, onKeys: Field) = InnerJoinClause(bucket, onKeys, this) + fun innerJoin(bucket: Bucket, onKey: Field, forBucket: Bucket) = InnerJoinClause(bucket, onKey, forBucket, this) fun leftJoin(bucket: Bucket, onCondition: TypeExpression) = LeftJoinClause(bucket, onCondition, this) fun leftJoin(bucket: Bucket, onKeys: Field) = LeftJoinClause(bucket, onKeys, this) + fun leftJoin(bucket: Bucket, onKey: Field, forBucket: Bucket) = LeftJoinClause(bucket, onKey, forBucket, this) fun rightJoin(bucket: Bucket, onCondition: TypeExpression) = RightJoinClause(bucket, onCondition, this) - fun rightJoin(bucket: Bucket, onKeys: Field) = RightJoinClause(bucket, onKeys, this) } interface ISelectUnnestClause : ISelectJoinClause { diff --git a/core/src/main/kotlin/ch/ergon/dope/resolvable/clause/model/JoinClause.kt b/core/src/main/kotlin/ch/ergon/dope/resolvable/clause/model/JoinClause.kt index 38d467e4..b2aa21bd 100644 --- a/core/src/main/kotlin/ch/ergon/dope/resolvable/clause/model/JoinClause.kt +++ b/core/src/main/kotlin/ch/ergon/dope/resolvable/clause/model/JoinClause.kt @@ -3,66 +3,95 @@ package ch.ergon.dope.resolvable.clause.model import ch.ergon.dope.DopeQuery import ch.ergon.dope.resolvable.clause.ISelectFromClause import ch.ergon.dope.resolvable.clause.ISelectJoinClause +import ch.ergon.dope.resolvable.clause.model.JoinType.INNER_JOIN +import ch.ergon.dope.resolvable.clause.model.JoinType.JOIN +import ch.ergon.dope.resolvable.clause.model.JoinType.LEFT_JOIN +import ch.ergon.dope.resolvable.clause.model.JoinType.RIGHT_JOIN import ch.ergon.dope.resolvable.expression.TypeExpression import ch.ergon.dope.resolvable.expression.unaliased.type.Field import ch.ergon.dope.resolvable.fromable.Bucket import ch.ergon.dope.validtype.BooleanType import ch.ergon.dope.validtype.ValidType +private enum class JoinType(val type: String) { + JOIN("JOIN"), + LEFT_JOIN("LEFT JOIN"), + INNER_JOIN("INNER JOIN"), + RIGHT_JOIN("RIGHT JOIN"), +} + sealed class SelectJoinClause : ISelectJoinClause { private val dopeQuery: DopeQuery - constructor(joinType: String, bucket: Bucket, onCondition: TypeExpression, parentClause: ISelectFromClause) { + constructor(joinType: JoinType, bucket: Bucket, onCondition: TypeExpression, parentClause: ISelectFromClause) { val parentDopeQuery = parentClause.toDopeQuery() val bucketDopeQuery = bucket.toDopeQuery() val onConditionDopeQuery = onCondition.toDopeQuery() dopeQuery = DopeQuery( - queryString = "${parentDopeQuery.queryString} $joinType ${bucketDopeQuery.queryString} ON ${onConditionDopeQuery.queryString}", + queryString = "${parentDopeQuery.queryString} ${joinType.type} ${bucketDopeQuery.queryString} " + + "ON ${onConditionDopeQuery.queryString}", parameters = parentDopeQuery.parameters + bucketDopeQuery.parameters + onConditionDopeQuery.parameters, ) } - constructor(joinType: String, bucket: Bucket, key: Field, parentClause: ISelectFromClause) { + constructor(joinType: JoinType, bucket: Bucket, onKeys: Field, parentClause: ISelectFromClause) { val parentDopeQuery = parentClause.toDopeQuery() val bucketDopeQuery = bucket.toDopeQuery() - val keyDopeQuery = key.toDopeQuery() + val keyDopeQuery = onKeys.toDopeQuery() dopeQuery = DopeQuery( - queryString = "${parentDopeQuery.queryString} $joinType ${bucketDopeQuery.queryString} ON KEYS ${keyDopeQuery.queryString}", + queryString = "${parentDopeQuery.queryString} ${joinType.type} ${bucketDopeQuery.queryString} " + + "ON KEYS ${keyDopeQuery.queryString}", parameters = parentDopeQuery.parameters + bucketDopeQuery.parameters + keyDopeQuery.parameters, ) } + constructor(joinType: JoinType, bucket: Bucket, onKey: Field, forBucket: Bucket, parentClause: ISelectFromClause) { + val parentDopeQuery = parentClause.toDopeQuery() + val bucketDopeQuery = bucket.toDopeQuery() + val keyDopeQuery = onKey.toDopeQuery() + val forBucketDopeQuery = forBucket.toDopeQuery() + dopeQuery = DopeQuery( + queryString = "${parentDopeQuery.queryString} ${joinType.type} ${bucketDopeQuery.queryString} " + + "ON KEY ${keyDopeQuery.queryString} FOR ${forBucketDopeQuery.queryString}", + parameters = parentDopeQuery.parameters + bucketDopeQuery.parameters + keyDopeQuery.parameters + forBucketDopeQuery.parameters, + ) + } + override fun toDopeQuery(): DopeQuery = dopeQuery } class StandardJoinClause : SelectJoinClause { constructor(bucket: Bucket, onCondition: TypeExpression, parentClause: ISelectFromClause) : - super("JOIN", bucket, onCondition, parentClause) + super(JOIN, bucket, onCondition, parentClause) constructor(bucket: Bucket, onKeys: Field, parentClause: ISelectFromClause) : - super("JOIN", bucket, onKeys, parentClause) + super(JOIN, bucket, onKeys, parentClause) + + constructor(bucket: Bucket, onKey: Field, forBucket: Bucket, parentClause: ISelectFromClause) : + super(JOIN, bucket, onKey, forBucket, parentClause) } class LeftJoinClause : SelectJoinClause { constructor(bucket: Bucket, onCondition: TypeExpression, parentClause: ISelectFromClause) : - super("LEFT JOIN", bucket, onCondition, parentClause) + super(LEFT_JOIN, bucket, onCondition, parentClause) constructor(bucket: Bucket, onKeys: Field, parentClause: ISelectFromClause) : - super("LEFT JOIN", bucket, onKeys, parentClause) + super(LEFT_JOIN, bucket, onKeys, parentClause) + + constructor(bucket: Bucket, onKey: Field, forBucket: Bucket, parentClause: ISelectFromClause) : + super(LEFT_JOIN, bucket, onKey, forBucket, parentClause) } class InnerJoinClause : SelectJoinClause { constructor(bucket: Bucket, onCondition: TypeExpression, parentClause: ISelectFromClause) : - super("INNER JOIN", bucket, onCondition, parentClause) + super(INNER_JOIN, bucket, onCondition, parentClause) constructor(bucket: Bucket, onKeys: Field, parentClause: ISelectFromClause) : - super("INNER JOIN", bucket, onKeys, parentClause) -} + super(INNER_JOIN, bucket, onKeys, parentClause) -class RightJoinClause : SelectJoinClause { - constructor(bucket: Bucket, onCondition: TypeExpression, parentClause: ISelectFromClause) : - super("RIGHT JOIN", bucket, onCondition, parentClause) - - constructor(bucket: Bucket, onKeys: Field, parentClause: ISelectFromClause) : - super("RIGHT JOIN", bucket, onKeys, parentClause) + constructor(bucket: Bucket, onKey: Field, forBucket: Bucket, parentClause: ISelectFromClause) : + super(INNER_JOIN, bucket, onKey, forBucket, parentClause) } + +class RightJoinClause(bucket: Bucket, onCondition: TypeExpression, parentClause: ISelectFromClause) : + SelectJoinClause(RIGHT_JOIN, bucket, onCondition, parentClause) diff --git a/core/src/test/kotlin/ch/ergon/dope/JoinClauseTest.kt b/core/src/test/kotlin/ch/ergon/dope/JoinClauseTest.kt index bfeabf85..e751509e 100644 --- a/core/src/test/kotlin/ch/ergon/dope/JoinClauseTest.kt +++ b/core/src/test/kotlin/ch/ergon/dope/JoinClauseTest.kt @@ -334,30 +334,54 @@ class JoinClauseTest { } @Test - fun `Use INDEX right join to flip the direction`() { - val expected = "SELECT DISTINCT `route`.`destinationairport`, " + - "`route`.`stops`, `route`.`airline`, `airline`.`name`, `airline`.`callsign` " + - "FROM `route` RIGHT JOIN `airline` ON KEYS `route`.`airlineid` " + - "WHERE `airline`.`icao` = \"SWA\" LIMIT 4" + fun `Use INDEX join to flip the direction with on key for`() { + val expected = "SELECT * FROM `airline` JOIN `route` ON KEY `route`.`airlineid` FOR `airline`" - val actual = create.selectDistinct( - someStringField("destinationairport", route), - someStringField("stops", route), - someStringField("airline", route), - someStringField("name", airline), - someStringField("callsign", airline), - ).from( - route, - ).rightJoin( - airline, - onKeys = someStringField("airlineid", route), - ).where( - someStringField("icao", airline).isEqualTo("SWA".toStringType()), - ).limit( - 4, - ).build().queryString + val actual: String = create + .selectAsterisk() + .from( + airline, + ).join( + route, + onKey = someStringField("airlineid", route), + forBucket = airline, + ).build().queryString - assertEquals(unifyString(expected), actual) + assertEquals(expected, actual) + } + + @Test + fun `Use INDEX inner join to flip the direction with on key for`() { + val expected = "SELECT * FROM `airline` INNER JOIN `route` ON KEY `route`.`airlineid` FOR `airline`" + + val actual: String = create + .selectAsterisk() + .from( + airline, + ).innerJoin( + route, + onKey = someStringField("airlineid", route), + forBucket = airline, + ).build().queryString + + assertEquals(expected, actual) + } + + @Test + fun `Use INDEX left join to flip the direction with on key for`() { + val expected = "SELECT * FROM `airline` LEFT JOIN `route` ON KEY `route`.`airlineid` FOR `airline`" + + val actual: String = create + .selectAsterisk() + .from( + airline, + ).leftJoin( + route, + onKey = someStringField("airlineid", route), + forBucket = airline, + ).build().queryString + + assertEquals(expected, actual) } @Test diff --git a/crystal-map-connector/src/main/kotlin/ch/ergon/dope/extension/clause/SelectClause.kt b/crystal-map-connector/src/main/kotlin/ch/ergon/dope/extension/clause/SelectClause.kt index 1b423b1f..439f9da5 100644 --- a/crystal-map-connector/src/main/kotlin/ch/ergon/dope/extension/clause/SelectClause.kt +++ b/crystal-map-connector/src/main/kotlin/ch/ergon/dope/extension/clause/SelectClause.kt @@ -34,12 +34,13 @@ fun ISelectWhereClause.groupBy(field: CMType, vararg fields: CMType): GroupByCla fun ISelectFromClause.where(whereExpression: CMField) = where(whereExpression.asField()) fun ISelectJoinClause.join(bucket: Bucket, onKeys: CMField) = join(bucket, onKeys.asField()) +fun ISelectJoinClause.join(bucket: Bucket, onKey: CMField, forBucket: Bucket) = join(bucket, onKey.asField(), forBucket) fun ISelectJoinClause.innerJoin(bucket: Bucket, onKeys: CMField) = innerJoin(bucket, onKeys.asField()) +fun ISelectJoinClause.innerJoin(bucket: Bucket, onKey: CMField, forBucket: Bucket) = innerJoin(bucket, onKey.asField(), forBucket) fun ISelectJoinClause.leftJoin(bucket: Bucket, onKeys: CMField) = leftJoin(bucket, onKeys.asField()) - -fun ISelectJoinClause.rightJoin(bucket: Bucket, onKeys: CMField) = rightJoin(bucket, onKeys.asField()) +fun ISelectJoinClause.leftJoin(bucket: Bucket, onKey: CMField, forBucket: Bucket) = leftJoin(bucket, onKey.asField(), forBucket) @JvmName("unnestString") fun ISelectUnnestClause.unnest(arrayField: CMList) = unnest(arrayField.asArrayField()) diff --git a/crystal-map-connector/src/test/kotlin/ch/ergon/dope/extensions/clause/SelectClauseTest.kt b/crystal-map-connector/src/test/kotlin/ch/ergon/dope/extensions/clause/SelectClauseTest.kt index 2e3e6a7e..25cc73fc 100644 --- a/crystal-map-connector/src/test/kotlin/ch/ergon/dope/extensions/clause/SelectClauseTest.kt +++ b/crystal-map-connector/src/test/kotlin/ch/ergon/dope/extensions/clause/SelectClauseTest.kt @@ -7,7 +7,6 @@ import ch.ergon.dope.extension.clause.leftJoin import ch.ergon.dope.extension.clause.limit import ch.ergon.dope.extension.clause.offset import ch.ergon.dope.extension.clause.orderBy -import ch.ergon.dope.extension.clause.rightJoin import ch.ergon.dope.extension.clause.unnest import ch.ergon.dope.extension.clause.where import ch.ergon.dope.helper.someBucket @@ -59,6 +58,13 @@ class SelectClauseTest { assertEquals("SELECT * FROM `someBucket` JOIN `other` ON KEYS `someNumberField`", actual) } + @Test + fun `should support select join on key for with CM`() { + val actual: String = someFrom().join(someBucket("other"), onKey = someCMNumberField(), someBucket()).toDopeQuery().queryString + + assertEquals("SELECT * FROM `someBucket` JOIN `other` ON KEY `someNumberField` FOR `someBucket`", actual) + } + @Test fun `should support select inner join with CM`() { val actual: String = someFrom().innerJoin(someBucket("other"), onKeys = someCMNumberField()).toDopeQuery().queryString @@ -66,6 +72,13 @@ class SelectClauseTest { assertEquals("SELECT * FROM `someBucket` INNER JOIN `other` ON KEYS `someNumberField`", actual) } + @Test + fun `should support select inner join on key for with CM`() { + val actual: String = someFrom().innerJoin(someBucket("other"), onKey = someCMNumberField(), someBucket()).toDopeQuery().queryString + + assertEquals("SELECT * FROM `someBucket` INNER JOIN `other` ON KEY `someNumberField` FOR `someBucket`", actual) + } + @Test fun `should support select left join with CM`() { val actual: String = someFrom().leftJoin(someBucket("other"), onKeys = someCMNumberField()).toDopeQuery().queryString @@ -74,10 +87,10 @@ class SelectClauseTest { } @Test - fun `should support select right join with CM`() { - val actual: String = someFrom().rightJoin(someBucket("other"), onKeys = someCMNumberField()).toDopeQuery().queryString + fun `should support select left join on key for with CM`() { + val actual: String = someFrom().leftJoin(someBucket("other"), onKey = someCMNumberField(), someBucket()).toDopeQuery().queryString - assertEquals("SELECT * FROM `someBucket` RIGHT JOIN `other` ON KEYS `someNumberField`", actual) + assertEquals("SELECT * FROM `someBucket` LEFT JOIN `other` ON KEY `someNumberField` FOR `someBucket`", actual) } @Test