Skip to content

Commit

Permalink
Merge pull request #187 from modelix/modelql2-doc
Browse files Browse the repository at this point in the history
Documentation of ModelQL v2
  • Loading branch information
slisson authored Oct 25, 2023
2 parents f9b40d6 + 405a00b commit 9c29adf
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 23 deletions.
33 changes: 33 additions & 0 deletions docs/global/modules/core/pages/explanation/modelql.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
= ModelQL

When working with large models you will quickly run into performance issues
when you try to replicate the whole model into the client.

While the data structure for model replication in Modelix supports partial loading of models,
you still need a way to describe which data you need on the client.
Loading data on demand while traversing the model also results in a poor performance,
because of the potentially large number of fine-grained request.

A first attempt to solve this problem was to disallow lazy loading
and require the client to load all required data at the beginning,
before working with the model.
A special query language was used to filter the data and an attempt to access a node that is not included by that query
resulted in an exception, forcing the developer to adjust the query.
While this results in a more predictable performance, it is also hard to maintain and still not optimal for the performance.
You have to download all the data at the beginning that you might eventually need, potentially exceeding the available memory of the system.

The ModelQL query language provides a more dynamic way of loading parts of the model on demand,
but still allows reducing the number of request to a minimum.
The downside is that it's not just a different implementation hidden behind the model-api,
but requires to use a different API.

== Reactive Streams

The query language is inspired by https://www.reactive-streams.org/[Reactive Streams]
and the execution engine uses https://kotlinlang.org/docs/flow.html[Kotlin Flows],
which is a https://kotlinlang.org/docs/coroutines-guide.html[Coroutines] compatible implementation of Reactive Streams.

Often it's useful to know if a stream is expected to return only one element or multiple elements.
https://projectreactor.io/[Project Reactor], another implementation of Reactive Streams,
introduced the notion of `Mono` and `Flux` to distinguish them.
You will also find them in ModelQL.
190 changes: 190 additions & 0 deletions docs/global/modules/core/pages/howto/modelql.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
= ModelQL

== Independent ModelQLClient

ModelQL defines its own HTTP endpoint and provides server/client implementations for it.
The `model-server` and the `mps-model-server-plugin` already implement this endpoint.
The client can be created like this:

[source,kotlin]
--
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val result: List<String?> = client.query { root ->
root.children("modules").property("name").toList()
}
--

== Integration with LightModelClient

When creating a `LightModelClient` you can optionally provide a `ModelQLClient` instance,
which allows invoking `.query { ... }` (see below) on a node returned by the `LightModelClient`.

[source,kotlin]
--
val modelqlClient = ModelQLClient.builder().build()
val client = LightModelClient.builder().modelQLClient(modelqlClient).build()
val result: List<String?> = client.getRootNode()!!.query {
it.children("modules").property("name").toList()
}
--

== Type-safe ModelQL API

You can use the `model-api-gen-gradle` plugin to generate type safe extensions from your meta-model.
Specify the link:../reference/component-model-api-gen-gradle.adoc#model-api-gen-gradle_attributes_modelqlKotlinDir[modelqlKotlinDir] property to enable the generation.

[source,kotlin]
--
val result: List<StaticMethodDeclaration> = client.query { root ->
root.children("classes").ofConcept(C_ClassConcept)
.member
.ofConcept(C_StaticMethodDeclaration)
.filter { it.visibility.instanceOf(C_PublicVisibility) }
.toList()
}
--

== Run query on an INode

If a query returns a node, you can execute a new query starting from that node.

[source,kotlin]
--
val cls: ClassConcept = client.query {
it.children("classes").ofConcept(C_ClassConcept).first()
}
val names = cls.query { it.member.ofConcept(C_StaticMethodDeclaration).name.toList() }
--

For convenience, it's possible to access further data of that node using the https://api.modelix.org/3.6.0/model-api/org.modelix.model.api/-i-node/index.html?query=interface%20INode[INode] API,
but this is not recommended though, because each access sends a new query to the server.

[source,kotlin]
--
val cls: ClassConcept = client.query {
it.children("classes").ofConcept(C_ClassConcept).first()
}
val className = cls.name
--

== Complex query results

While returning a list of elements is simple,
the purpose of the query language is to reduce the number of request to a minimum.
This requires combining multiple values into more complex data structures.
The `zip` operation provides a simple way of doing that:

[source,kotlin]
--
val result: List<IZip3Output<Any, Int, String, List<String>>> = query { db ->
db.products.map {
val id = it.id
val title = it.title
val images = it.images.toList()
id.zip(title, images)
}.toList()
}
result.forEach { println("ID: ${it.first}, Title: ${it.second}, Images: ${it.third}") }
--

This is suitable for combining a small number of values,
but because of the missing variable names it can be hard to read for a larger number of values
or even multiple zip operations assembled into a hierarchical data structure.

This can be solved by defining custom data classes and using the `mapLocal` operation:

[source,kotlin]
--
data class MyProduct(val id: Int, val title: String, val images: List<MyImage>)
data class MyImage(val url: String)

val result: List<MyProduct> = remoteProductDatabaseQuery { db ->
db.products.map {
val id = it.id
val title = it.title
val images = it.images.mapLocal { MyImage(it) }.toList()
id.zip(title, images).mapLocal {
MyProduct(it.first, it.second, it.third)
}
}.toList()
}
result.forEach { println("ID: ${it.id}, Title: ${it.title}, Images: ${it.images}") }
--

The `mapLocal` operation is not just useful in combination with the `zip` operation,
but in general to create instances of classes only known to the client.

The body of `mapLocal` is executed on the client after receiving the result from the server.
That's why you only have access to the output of the `zip` operation
and still have to use `first`, `second` and `third` inside the query.

To make this even more readable there is a `buildLocalMapping` operation,
which provides a different syntax for the `zip`-`mapLocal` chain.

[source,kotlin]
--
data class MyProduct(val id: Int, val title: String, val images: List<MyImage>)
data class MyImage(val url: String)

val result: List<MyProduct> = query { db ->
db.products.buildLocalMapping {
val id = it.id.request()
val title = it.title.request()
val images = it.images.mapLocal { MyImage(it) }.toList().request()
onSuccess {
MyProduct(id.get(), title.get(), images.get())
}
}.toList()
}
result.forEach { println("ID: ${it.id}, Title: ${it.title}, Images: ${it.images}") }
--

At the beginning of the `buildLocalMapping` body, you invoke `request()` on all the values you need to assemble your object.
This basically adds the operand to the internal `zip` operation and returns an object that gives you access to the value
after receiving it from the server.
Inside the `onSuccess` block you assemble the local object using the previously requested values.

== Kotlin HTML integration

One use case of the query language is to build database applications
that generate HTML pages from the data stored in the model server.
You can use the https://kotlinlang.org/docs/typesafe-html-dsl.html[Kotlin HTML DSL] together with ModelQL to do that.

Use `buildHtmlQuery` to request data from the server and render it into an HTML string:

[source,kotlin]
--
val html = query {
it.map(buildHtmlQuery {
val modules = input.children("modules").requestFragment<_, FlowContent> {
val moduleName = input.property("name").request()
val models = input.children("models").requestFragment<_, FlowContent> {
val modelName = input.property("name").request()
onSuccess {
div {
h2 {
+"Model: ${modelName.get()}"
}
}
}
}
onSuccess {
div {
h1 {
+"Module: ${moduleName.get()}"
}
insertFragment(models)
}
}
}
onSuccess {
body {
insertFragment(modules)
}
}
})
}
--

`buildHtmlQuery` and the `requestFragment` operation are similar to the `buildLocalMapping` operation,
but inside the `onSuccess` block you use the Kotlin HTML DSL.
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Inside of the `metamodel` block the following settings can be configured.
|File
|Target Kotlin directory of the generator

|`modelqlKotlinDir`
|`modelqlKotlinDir` [[model-api-gen-gradle_attributes_modelqlKotlinDir,modelqlKotlinDir]]
|File
|The generation of the ModelQL API is optional, because the output has a dependency on the ModelQL runtime.
If this option is set, you have to add a dependency on `org.modelix:modelql-typed`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class TypedModelQLTest {

@Test
fun simpleTest() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val result: Int = client.query { root ->
root.children("classes").ofConcept(C_ClassConcept)
.member
Expand All @@ -118,7 +118,7 @@ class TypedModelQLTest {

@Test
fun test() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val result: List<Pair<String, String>> = client.query { root ->
root.children("classes").ofConcept(C_ClassConcept)
.member
Expand All @@ -132,7 +132,7 @@ class TypedModelQLTest {

@Test
fun testReferences() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val usedVariables: Set<String> = client.query { root ->
root.children("classes").ofConcept(C_ClassConcept)
.member
Expand All @@ -148,7 +148,7 @@ class TypedModelQLTest {

@Test
fun testReferencesFqName() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val usedVariables: Set<String> = client.query { root ->
root.children("classes").ofConcept(C_ClassConcept)
.member
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class HtmlBuilderTest {

@Test
fun modular() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()

val modelTemplate = buildModelQLFragment<INode, FlowContent> {
val name = input.property("name").getLater()
Expand Down Expand Up @@ -121,7 +121,7 @@ class HtmlBuilderTest {

@Test
fun recursive() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()

val modelTemplate = buildModelQLFragment<INode, FlowContent> {
val name = input.property("name").getLater()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class ModelQLClientTest {

@Test
fun test_count() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val result: Int = client.query { root ->
root.allChildren().count()
}
Expand All @@ -88,7 +88,7 @@ class ModelQLClientTest {

@Test
fun test_properties() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val result: List<String?> = client.query { root ->
root.children("modules").property("name").toList()
}
Expand All @@ -97,7 +97,7 @@ class ModelQLClientTest {

@Test
fun test_zip() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val result = client.query { root ->
root.children("modules").map {
it.property("name").zip(it.allChildren().nodeReference().toList())
Expand All @@ -107,7 +107,7 @@ class ModelQLClientTest {

@Test
fun test_zipN() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val result = client.query { root ->
root.children("modules").map {
it.property("name").zip(
Expand All @@ -123,7 +123,7 @@ class ModelQLClientTest {

@Test
fun writeProperty() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val updatesNodes = client.query { root ->
root.children("modules")
.children("models").filter { it.property("name").contains("model1a") }
Expand All @@ -148,7 +148,7 @@ class ModelQLClientTest {

@Test
fun writeReference() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val updatedNodes = client.query { root ->
root.children("modules")
.children("models").filter { it.property("name").contains("model1a") }
Expand All @@ -167,7 +167,7 @@ class ModelQLClientTest {

@Test
fun addNewChild() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val createdNodes = client.query { root ->
root.children("modules")
.children("models")
Expand All @@ -188,7 +188,7 @@ class ModelQLClientTest {

@Test
fun removeNode() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()

suspend fun countModels(): Int {
return client.query { root ->
Expand All @@ -214,7 +214,7 @@ class ModelQLClientTest {

@Test
fun recursiveQuery() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()

val descendantsNames: IFluxUnboundQuery<INode, String?> = buildFluxQuery<INode, String?> {
it.property("name") + it.allChildren().mapRecursive()
Expand All @@ -229,7 +229,7 @@ class ModelQLClientTest {

@Test
fun testCaching() = runTest { httpClient ->
val client = ModelQLClient("http://localhost/query", httpClient)
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()

val result: List<Int> = client.query { root ->
val numberOfNodes = root.descendants()
Expand Down
Loading

0 comments on commit 9c29adf

Please sign in to comment.