Skip to content

Commit 4eb8ccb

Browse files
committed
feat(modelql): new query language for working with models without replication
When working with large models there is the problem that you cannot or don't want to replicate the whole model into the client. All the existing approaches of dealing with partially replicated models are designed for a real-time collaboration use case. But there are use cases that are more similar to a database application where you render an HTML page with a summary of the data stored on the model server. The new ModelQL API is a query language similar to what database systems provide. It's a powerful way of describing the data you are interested in and then receiving just that. There is no active connection that listens for changes and no caching required. It reduces the complexity and solves some of the performance issues for these use cases.
1 parent 860956f commit 4eb8ccb

File tree

123 files changed

+11416
-23
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

123 files changed

+11416
-23
lines changed

commitlint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module.exports = {
1515
"model-client",
1616
"model-server",
1717
"model-sync-lib",
18+
"modelql",
1819
"mps-model-adapters",
1920
"mps-model-server",
2021
"mps-model-server-plugin",

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ apacheCxf="3.5.5"
2929
[libraries]
3030

3131
kotlin-stdlib-common = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-common", version.ref = "kotlin" }
32+
kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" }
3233
kotlin-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinSerialization" }
3334
kotlin-serialization-yaml = { group = "com.charleskorn.kaml", name = "kaml", version = "0.55.0" }
3435
kotlin-logging = { group = "io.github.microutils", name = "kotlin-logging", version = "3.0.5" }

model-api-gen-gradle-test/build.gradle.kts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,39 @@ val mpsDir = buildDir.resolve("mps")
3838

3939
val modelixCoreVersion: String = projectDir.resolve("../version.txt").readText()
4040

41+
val kotlinGenDir = buildDir.resolve("metamodel/kotlin_gen")
42+
4143
dependencies {
4244
mps("com.jetbrains:mps:2021.1.4")
4345
implementation("org.modelix:model-api-gen-runtime:$modelixCoreVersion")
46+
implementation("org.modelix:modelql-typed:$modelixCoreVersion")
47+
implementation("org.modelix:modelql-untyped:$modelixCoreVersion")
48+
49+
testImplementation(kotlin("test-common"))
50+
testImplementation(kotlin("test-annotations-common"))
4451
testImplementation(kotlin("test"))
52+
testImplementation(kotlin("reflect"))
53+
4554
testImplementation("org.modelix:model-api:$modelixCoreVersion")
4655
testImplementation("org.modelix:model-client:$modelixCoreVersion")
47-
testImplementation(files("$buildDir/metamodel/kotlin_gen") {
56+
testImplementation("org.modelix:model-server-lib:$modelixCoreVersion")
57+
testImplementation("org.modelix:modelql-client:$modelixCoreVersion")
58+
59+
testImplementation(libs.ktor.server.core)
60+
testImplementation(libs.ktor.server.cors)
61+
testImplementation(libs.ktor.server.netty)
62+
testImplementation(libs.ktor.server.html.builder)
63+
testImplementation(libs.ktor.server.auth)
64+
testImplementation(libs.ktor.server.auth.jwt)
65+
testImplementation(libs.ktor.server.status.pages)
66+
testImplementation(libs.ktor.server.forwarded.header)
67+
testImplementation(libs.ktor.server.websockets)
68+
testImplementation(libs.ktor.server.test.host)
69+
testImplementation(libs.logback.classic)
70+
71+
testImplementation(files(kotlinGenDir) {
4872
builtBy("generateMetaModelSources")
4973
})
50-
testImplementation(kotlin("reflect"))
5174
}
5275

5376
tasks.test {
@@ -59,7 +82,6 @@ val resolveMps by tasks.registering(Sync::class) {
5982
into(mpsDir)
6083
}
6184

62-
val kotlinGenDir = buildDir.resolve("metamodel/kotlin_gen")
6385
sourceSets["main"].kotlin {
6486
srcDir(kotlinGenDir)
6587
}
@@ -80,6 +102,7 @@ metamodel {
80102
typedNode.prefix = ""
81103
typedNodeImpl.suffix = "Impl"
82104
}
105+
registrationHelperName = "org.modelix.apigen.test.ApigenTestLanguages"
83106
}
84107

85108
node {

model-api-gen-gradle-test/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

model-api-gen-gradle-test/settings.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,17 @@ pluginManagement {
1111
maven { url = uri("https://artifacts.itemis.cloud/repository/maven-mps/") }
1212
mavenCentral()
1313
}
14+
dependencyResolutionManagement {
15+
repositories {
16+
mavenLocal()
17+
gradlePluginPortal()
18+
maven { url = uri("https://artifacts.itemis.cloud/repository/maven-mps/") }
19+
mavenCentral()
20+
}
21+
versionCatalogs {
22+
create("libs") {
23+
from("org.modelix:core-version-catalog:$modelixCoreVersion")
24+
}
25+
}
26+
}
1427
}

model-api-gen-gradle-test/src/test/kotlin/GeneratedApiTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import jetbrains.mps.lang.behavior.ConceptMethodDeclaration
77
import jetbrains.mps.lang.core.L_jetbrains_mps_lang_core
88
import jetbrains.mps.lang.editor.*
99
import jetbrains.mps.lang.smodel.query.CustomScope_old
10-
import org.junit.jupiter.api.BeforeAll
1110
import org.modelix.metamodel.*
1211
import org.modelix.model.ModelFacade
1312
import org.modelix.model.api.INode
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.modelix.modelql.typed
15+
16+
import io.ktor.client.HttpClient
17+
import io.ktor.server.testing.testApplication
18+
import jetbrains.mps.baseLanguage.C_ClassConcept
19+
import jetbrains.mps.baseLanguage.C_IntegerType
20+
import jetbrains.mps.baseLanguage.C_PlusExpression
21+
import jetbrains.mps.baseLanguage.C_PublicVisibility
22+
import jetbrains.mps.baseLanguage.C_ReturnStatement
23+
import jetbrains.mps.baseLanguage.C_StaticMethodDeclaration
24+
import jetbrains.mps.baseLanguage.C_VariableReference
25+
import jetbrains.mps.baseLanguage.ClassConcept
26+
import jetbrains.mps.baseLanguage.StaticMethodDeclaration
27+
import org.modelix.apigen.test.ApigenTestLanguages
28+
import org.modelix.metamodel.typed
29+
import org.modelix.model.api.IBranch
30+
import org.modelix.model.api.PBranch
31+
import org.modelix.model.api.getRootNode
32+
import org.modelix.model.api.resolve
33+
import org.modelix.model.client.IdGenerator
34+
import org.modelix.model.lazy.CLTree
35+
import org.modelix.model.lazy.ObjectStoreCache
36+
import org.modelix.model.persistent.MapBaseStore
37+
import org.modelix.model.server.light.LightModelServer
38+
import org.modelix.modelql.client.ModelQLClient
39+
import org.modelix.modelql.core.count
40+
import org.modelix.modelql.core.filter
41+
import org.modelix.modelql.core.map
42+
import org.modelix.modelql.core.toList
43+
import org.modelix.modelql.core.toSet
44+
import org.modelix.modelql.core.zip
45+
import org.modelix.modelql.gen.jetbrains.mps.baseLanguage.member
46+
import org.modelix.modelql.gen.jetbrains.mps.baseLanguage.parameter
47+
import org.modelix.modelql.gen.jetbrains.mps.baseLanguage.variableDeclaration
48+
import org.modelix.modelql.gen.jetbrains.mps.baseLanguage.visibility
49+
import org.modelix.modelql.gen.jetbrains.mps.lang.core.name
50+
import org.modelix.modelql.untyped.children
51+
import org.modelix.modelql.untyped.conceptReference
52+
import kotlin.test.BeforeTest
53+
import kotlin.test.Test
54+
import kotlin.test.assertEquals
55+
56+
class TypedModelQLTest {
57+
private lateinit var branch: IBranch
58+
59+
private fun runTest(block: suspend (HttpClient) -> Unit) = testApplication {
60+
application {
61+
LightModelServer(80, branch.getRootNode()).apply { installHandlers() }
62+
}
63+
val httpClient = createClient {
64+
}
65+
block(httpClient)
66+
}
67+
68+
@BeforeTest
69+
fun setup() {
70+
ApigenTestLanguages.registerAll()
71+
val tree = CLTree(ObjectStoreCache(MapBaseStore()))
72+
branch = PBranch(tree, IdGenerator.getInstance(1))
73+
val rootNode = branch.getRootNode()
74+
branch.runWrite {
75+
val cls1 = rootNode.addNewChild("classes", -1, C_ClassConcept.untyped()).typed<ClassConcept>()
76+
cls1.apply {
77+
name = "Math"
78+
member.addNew(C_StaticMethodDeclaration).apply {
79+
name = "plus"
80+
returnType.setNew(C_IntegerType)
81+
visibility.setNew(C_PublicVisibility)
82+
val a = parameter.addNew().apply {
83+
name = "a"
84+
type.setNew(C_IntegerType)
85+
}
86+
val b = parameter.addNew().apply {
87+
name = "b"
88+
type.setNew(C_IntegerType)
89+
}
90+
body.setNew().apply {
91+
statement.addNew(C_ReturnStatement).apply {
92+
expression.setNew(C_PlusExpression).apply {
93+
leftExpression.setNew(C_VariableReference).apply {
94+
variableDeclaration = a
95+
}
96+
rightExpression.setNew(C_VariableReference).apply {
97+
variableDeclaration = b
98+
}
99+
}
100+
}
101+
}
102+
}
103+
}
104+
}
105+
}
106+
107+
@Test
108+
fun simpleTest() = runTest { httpClient ->
109+
val client = ModelQLClient("http://localhost/query", httpClient)
110+
val result: Int = client.query { root ->
111+
root.children("classes").ofConcept(C_ClassConcept)
112+
.member
113+
.ofConcept(C_StaticMethodDeclaration)
114+
.count()
115+
}
116+
assertEquals(1, result)
117+
}
118+
119+
@Test
120+
fun test() = runTest { httpClient ->
121+
val client = ModelQLClient("http://localhost/query", httpClient)
122+
val result: List<Pair<String, String>> = client.query { root ->
123+
root.children("classes").ofConcept(C_ClassConcept)
124+
.member
125+
.ofConcept(C_StaticMethodDeclaration)
126+
.filter { it.visibility.instanceOf(C_PublicVisibility) }
127+
.map { it.name.zip(it.parameter.name.toList(), it.untyped().conceptReference()) }
128+
.toList()
129+
}.map { it.first to it.first + "(" + it.second.joinToString(", ") + ") [" + it.third?.resolve()?.getLongName() + "]" }
130+
assertEquals(listOf("plus" to "plus(a, b) [jetbrains.mps.baseLanguage.StaticMethodDeclaration]"), result)
131+
}
132+
133+
@Test
134+
fun testReferences() = runTest { httpClient ->
135+
val client = ModelQLClient("http://localhost/query", httpClient)
136+
val usedVariables: Set<String> = client.query { root ->
137+
root.children("classes").ofConcept(C_ClassConcept)
138+
.member
139+
.ofConcept(C_StaticMethodDeclaration)
140+
.descendants()
141+
.ofConcept(C_VariableReference)
142+
.variableDeclaration
143+
.name
144+
.toSet()
145+
}
146+
assertEquals(setOf("a", "b"), usedVariables)
147+
}
148+
149+
@Test
150+
fun testReferencesFqName() = runTest { httpClient ->
151+
val client = ModelQLClient("http://localhost/query", httpClient)
152+
val usedVariables: Set<String> = client.query { root ->
153+
root.children("classes").ofConcept(C_ClassConcept)
154+
.member
155+
.ofConcept(C_StaticMethodDeclaration)
156+
.map { method ->
157+
method.descendants()
158+
.ofConcept(C_VariableReference)
159+
.variableDeclaration
160+
.name
161+
.toSet()
162+
.zip(method.name)
163+
}
164+
.toList()
165+
}.map { it.first.map { simpleName -> it.second + "." + simpleName } }.flatten().toSet()
166+
167+
assertEquals(setOf("plus.a", "plus.b"), usedVariables)
168+
169+
// TODO simplify query
170+
}
171+
172+
@Test
173+
fun testNodeSerialization() = runTest { httpClient ->
174+
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
175+
val result: List<StaticMethodDeclaration> = client.query { root ->
176+
root.children("classes").ofConcept(C_ClassConcept)
177+
.member
178+
.ofConcept(C_StaticMethodDeclaration)
179+
.filter { it.visibility.instanceOf(C_PublicVisibility) }
180+
.untyped()
181+
.toList()
182+
}.map { it.typed<StaticMethodDeclaration>() }
183+
assertEquals("plus", branch.computeRead { result[0].name })
184+
}
185+
186+
@Test
187+
fun returnTypedNode() = runTest { httpClient ->
188+
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
189+
val result: List<StaticMethodDeclaration> = client.query { root ->
190+
root.children("classes").ofConcept(C_ClassConcept)
191+
.member
192+
.ofConcept(C_StaticMethodDeclaration)
193+
.filter { it.visibility.instanceOf(C_PublicVisibility) }
194+
.toList()
195+
}
196+
assertEquals("plus", branch.computeRead { result[0].name })
197+
}
198+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<configuration debug="true">
3+
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
4+
<encoder>
5+
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
6+
</encoder>
7+
</appender>
8+
9+
<root level="DEBUG">
10+
<appender-ref ref="console" />
11+
</root>
12+
</configuration>

model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/ChildAccessor.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ abstract class ChildAccessor<ChildT : ITypedNode>(
3838
return parent.addNewChild(role, index, concept.untyped()).typed(concept.getInstanceClass())
3939
}
4040

41+
fun <NewNodeT : ChildT> addNew(concept: INonAbstractConcept<NewNodeT>): NewNodeT {
42+
return addNew(-1, concept)
43+
}
44+
4145
fun removeUnwrapped(child: INode) {
4246
parent.removeChild(child)
4347
}

0 commit comments

Comments
 (0)