-
Notifications
You must be signed in to change notification settings - Fork 27
Functionally merging multiple (partial) queries
When you are working in a functional codebase, you ofcourse want to be able to keep your code to be composable and abstract.
The query DSL allows the use of query combinator operators
.
Combinators can be used endlessly, together and in any order. E.g.:
select(a) :+> where(b) <+: from(c) + limit(Limit(size(10)))
Conflict resolving happens per part
and thus not per column whatsoever.
By part
we refer to "SELECT, FROM, FINAL, WHERE, GROUPBY etc. etc."
So 2 queries with a select block set will be in conflict, and depending on the operator you choose it will try to resolve the conflict by overwriting one over the other.
A join is in itself regarded as a part
and is not resolved by its parts recursively. This means that this has to be done separately, which is by design, and should still allow for 100% composability.
The :+>
operator takes two Query
objects and returns a merged version of this, choosing the part of the left query when this part is defined on both queries.
it should "merge with left preference" in {
val query = select(shieldId) from OneTestTable
val query2 = select(itemId) from OneTestTable where col2 >= 2
val composed = query :+> query2
clickhouseTokenizer.toSql(composed.internalQuery) should be (
s"SELECT shield_id FROM test.OneTestTable WHERE column_2 >= 2 FORMAT JSON"
)
}
The <+:
operator takes two Query
objects and returns a merged version of this, choosing the part of the right query when this part is defined on both queries.
it should "merge with right preference" in {
val query = select(shieldId) from OneTestTable
val query2 = select(itemId) from OneTestTable where col2 >= 2
val composed = query <+: query2
clickhouseTokenizer.toSql(composed.internalQuery) should be (
s"SELECT item_id FROM test.OneTestTable WHERE column_2 >= 2 FORMAT JSON"
)
}
The +
operator takes two Query
or Try[Query]
objects and returns a Try
which succeeds when theres no conflict, and fails otherwise.
it should "fail on try merge of conflicting queries" in {
val query = select(shieldId) from OneTestTable
val query2 = select(itemId) from OneTestTable where col2 >= 2
val composed = query + query2
composed should matchPattern {
case Failure(_:IllegalArgumentException) =>
}
}
it should "succeed on safe override of non-conflicting multi part queries" in {
val query = select(shieldId)
val query2 = from(OneTestTable)
val query3 = where(col2 >= 4)
val composed = query + query2
val composed2 = composed + query3
composed should matchPattern {
case t: Success[_] =>
}
clickhouseTokenizer.toSql(composed.get.internalQuery) should be (
"SELECT shield_id FROM test.OneTestTable FORMAT JSON"
)
composed2 should matchPattern {
case t: Success[_] =>
}
clickhouseTokenizer.toSql(composed2.get.internalQuery) should be (
"SELECT shield_id FROM test.OneTestTable WHERE column_2 >= 4 FORMAT JSON"
)
}
Todo pages:
- Table schemas
- SELECT statements
- Simple SELECT
- DISTINCT
- Inner queries
- JOIN
- GROUP
- Array operators
- Aggregation operators (e.g. uniqState, uniqMerge)
- COMPOSING of multiple queries
- Composition operators
<+:
,+
and:+>
- Composition operators
- Using custom types in the DSL
- Explaining the query parsing process
- The QueryValue typeclass