Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

VIATRA/Query/UserDocumentation/Examples/school

Introductory example: school

The school example features a simple EMF model and some simple graph patterns to show the very basics of using VIATRA Query. Read this first before anything else!


Overview

The aim of this page is to provide an easy-to-understand, step-by-step guide to VIATRA Query. For this purpose, the school example features a simple EMF model and some simple graph patterns to show the very basics of the query language and the core add-ons.


Obtaining the example

You will need two Eclipse workspaces to try this example.

You can launch the (school, school.edit, school.editor) projects in a Runtime Eclipse application (which will use the runtime workspace), or you can install them into your host Eclipse (in that case, no separate runtime workspace is necessary).


Source projects

  • host workspace / installed plugins: school, school.edit, school.editor

The school project contains the EMF metamodel (school.ecore) for representing schools, years, classes, students, teachers, and courses. The .edit and .editor projects are automatically generated by EMF (no manual tweaking on the school.genmodel was performed.)

EMFIncQuery-Examples-school-Slide30.png

Example query and instance model contents

  • runtime workspace: school.instancemodel/

This project contains two school instance models with a school, years, classes, students, teachers, and courses. You can create your own model using the standard generated EMF features: New/Other/Example EMF Model Creation Wizards/School Model. BUTE.school contains the following configuration (some details, such as attribute values, have been omitted for the sake of clarity):


EMFIncQuery-Examples-school-Slide31.png

EMFIncQuery-Examples-school-Slide32.png


Hogwarts.school is another simple instance model example, borrowing some characters from a famous novel.

  • runtime workspace: school.query/

This project is an VIATRA Query Project (created with New/Other/VIATRA/Query Project). It also contains the tutorial queries (patterns) in the src/school/simpleSchoolQueries.vql file, which has been created using the New/Other/VIATRA/Query Definition wizard.


Jump right in - try the patterns!

  1. Make sure you are in the runtime workspace.
  2. Make sure the "Query Explorer" view of VIATRA is open (Window/Show view/VIATRA/Query Explorer).
  3. In the school.query project, right click in the simpleSchoolQueries.vql file and choose Register patterns in Query Explorer.
    1. Alternatively, you can use the magical green button of the Query Explorer view (green triangle in the view toolbar) when the simpleSchoolQueries.vql file is the active editor.
  4. Observe that the patterns have been registered by opening the left-side panel (Pattern registry) of the Query Explorer.
  5. Open BUTE.school file (either with the generated School editor, or the Sample Reflective Ecore model editor, both of them should work).
  6. Import the instance model into the Query Explorer by using the magical green button again (while the instance model editor is the active editor).
  7. Observe that the instance model has been loaded; expand the tree to see each pattern and their matches.

You should see something like this:


EMFIncQuery-Examples-school-iqexplorer.png


Observe that both the Pattern Registry and the Details/Filters panels can be toggled, depending on whether you prefer to see their contents or not. The Details/Filters panel is also useful for parameterized (parameter-bound) queries (see later). If you double-click on a match (green dot icons), the selection corresponding to the match (containing all model elements referenced by pattern parameters) will be applied to the host editor (revealing the corresponding model elements automatically). This is useful if you want to locate the model elements in the editor.


Detailed step-by-step guide

Now we describe step-by-step how the sample queries/patterns have been created and what they do. The 7-step tutorial is intended to serve as a quick overview of the language and tooling features. To ease understanding, it is always a good idea to check the language overview and the FAQ. For performance considerations, be sure to check the performance guide.


Step 0 - header

package school
 
import "http://school.ecore"


When you first create an VIATRA Query query definition, an empty file will be created, with only a package declaration. The package declaration is primarily used to determine the Java packages of the code that VIATRA Query will generate.

In order to start working with VIATRA Query, you need to import an Ecore namespace, by typing the import keyword and then activating content assist (ctrl-space) and selecting a registered EPackage. Just after you have entered an import declaration, it is a good idea to include an appropriate plugin dependency to the contributing EMF .model plugin (in this case, school) in the MANIFEST.MF of the VIATRA Query project, and also take care to re-export this dependency.

Workspace EPackages will be usable with the VIATRA Query Generator Model (generator.vqlgen) by pointing to the genmodel file of the used metamodels. Once you add the resource URI to the eiqgen file, you can import with the nsUri in the vql file.


Step 1 - simple queries

// Step 1: simple queries
 
 /* 
  * all schools
  */
 pattern schools(Sch) = {
 	School(Sch);
 }
 
 /*
  *  all teachers
  */
 pattern teachers(T) = {
 	Teacher(T);
 }
 
 /* 
  * a sample query along a reference
  * 
  * Teacher T teaches in school Sch.
  */
 pattern teachersOfSchool(T:Teacher,Sch:School) = {
 	School.teachers(Sch,T); 
 	// note that there is no restriction on the direction of navigation:
 	// all "Sch"s can be queried from Ts and vice-versa
 }


The most simple queries in VIATRA Query are graph patterns that identify nodes and edges of the EMF instance model.

  • nodes (EObjects) can be identified by their Ecore types, such as School or Teacher. A simple pattern consisting of a single node type constraint will yield all instances of that type in the query results, called match set in VIATRA Query terminology.
  • edges (EReference "instances", i.e. Java references) can be added as additional structural constraints by their Ecore types, such as School.teachers.
    • in the teachersOfSchool pattern above, we are looking for pairs of School-Teacher instances that are actually connected by a teachers edge.
    • Notice that VIATRA Query has type inference: as the types of the edges endpoints can be inferred from the metamodel, there is no need to include additional Teacher(T) and School(Sch) constraints. The types are defined in the pattern header only for the sake of end-user readability.
    • Notice that VIATRA Query implicitly makes all EReference navigation bidirectional: while you have to formulate the pattern in a syntactially correct way (specifying that the teachers edge is defined for the School EClass), the query evaluates all connected School-Teacher pairs and thus can be used to look for the Schools belonging to a particular Teacher instance (thus navigating backward along the teachers edge). This works regardless whether there is an EOpposite relationship defined or not.


Step 2 - pattern calls, negative application conditions

// Step 2: pattern calls, negative application conditions
 
/* 
 * Courses of a teacher.
 * 
 * Teacher T teaches in Course C
 */
 pattern coursesOfTeacher(T:Teacher, C:Course) = {
 	Teacher.courses(T,C);
 } 
 
/*
 * Teacher T teaches a course which is being taught to SchoolClass SC 
 * Note: header type constraints can be ommitted due to automatic type inference.
 */
 pattern classesOfTeacher(T, SC) = {
 	find coursesOfTeacher(T,C);
 	Course.schoolClass(C,SC);
 }
 
 /*
  * Teacher T is a teacher who doesn't teach any schoolclass.
  */
 pattern teacherWithoutClass(T:Teacher) = {
	neg find classesOfTeacher(T,SC); 	
 }


One of the most powerful reusability features of the VIATRA Query pattern language is the find keyword. You can use it to call patterns from other patterns, giving you the ability to factor out commonly used code segments and reuse them whereever necessary. The find keyword uses an intuitive parameter passing semantics, identifying (bound) objects by the parameter they are assigned to (i.e., in the example, wherever you write C, it means the same object).

The find keyword can be preceded by the neg modifier to invert the meaning, i.e. forbid the match of the called pattern with the given parameterization. In that case, unbound parameters (SC) will be "running parameters", i.e. they are not allowed to be matched by anything.


Step 3 - path expressions

// Step 3: path expressions
 
 /*
  * Student S attends school Sch
  */
 pattern studentOfSchool(S:Student,Sch:School) = {
 	Student.schoolClass.courses.school(S,Sch);
 	// logically equivalent: School.courses.schoolClass.students(Sch,S);
 }


Path expressions are useful to compacting patterns, wherever intermediate nodes along a path are not important. Notice that in this particular case, the direction of navigation is unimportant in the sense that both ways of connecting S and Sch yield the same result, because all the included EReferences have EOpposites.


Step 4 - check conditions

// Step 4: check conditions
 
 /*
  * Attribute value constraint with a constant:
  * 
  * Course C has weight 30
  */
 pattern courseWithWeightThirty(C:Course) = {
 	Course.weight(C,30);
 }
 
 /*
  *  a bit more complicated check expression:
  * 
  * Course C has a subject name which is longer than C's weight.
  */
 pattern courseWithNameLongerThanWeight(C) {
	Course.subject(C, CName);
	Course.weight(C, W);
	check(CName.length > W); // no type casts necessary inside check expressions!
}
 
  /*
   * A check condition with some Xbase magic:
   * (this must be the most primitive prime tester ever :-)
   *
   * for the limitations on check() expressions, see https://viatra.inf.mit.bme.hu/incquery/language#Limitations
   * for details on Xbase expressions, see http://www.eclipse.org/Xtext/documentation/2_1_0/199h-xbase-language-reference-expressions.php 
   * 
   * RULES:
   * - ensure that a boolean value will be returned on all execution paths
   * - write side-effect free code
   * - only access pattern variables and their members, no navigation inside check() expression
   * Note: For v.0.6, the last two constraints are not hard-enforced and not checked, but this will change for v0.7.
   *
   * Course C has a weight which is a prime number
   */
pattern courseWithPrimeWeight(C) {
	Course.weight(C, W);
	check( { // make sure the check() expression returns a boolean value on all execution paths
		if(W % 2 == 0)
		false else {
			var Integer maxValue = Math:: round(new Float(Math:: sqrt(W)))
			var Integer divisor = 3 while(divisor <= maxValue) {
				if(W % divisor == 0)
				return false else divisor = divisor + 2;
			}
 
			true // we have found a prime number	
		}
 
	});
}


Check expressions are useful for specifying value constraints, most frequently for EAttributes. Technically, you write Xbase into a check() expression, making it possible to include complex logic right inside the query definition.

  • For the limitations on check() expressions, see the language reference
    • 0.6 limitation: the basic rule of thumb is that you should never include navigation inside a check expression; only write (side effect free) code that accesses a single object from the pattern body and make sure your expression returns a boolean value in a deterministic and idempotent way (that is, the same value for the same input, independently of when it is invoked).
    • 0.6 limitation: once VIATRA Query will have full type inference, the type casts (as) inside check() expressions will not be necessary.
  • For details on Xbase expressions, see the Xbase reference


Step 5 - counting

// Step 5: counting (match cardinalities)
 
  /*
   * Teacher T1 teaches more classes than teacher T2
   */
  @QueryExplorer(checked = false) // this removes it from the Query Explorer
  pattern teachesMoreClasses(T1 : Teacher, T2 : Teacher) {
	N == count find classesOfTeacher(T1, _SC1);
	M == count find classesOfTeacher(T2, _SC2);
	check(N > M);
  }
 
  /*
   * Teacher T teaches the most courses.
   * 
   * Note: there can be multiple matches to T as there might be cases
   * where multiple teachers teach the same and maximum amount of courses.
   */
  pattern teachesTheMostCourses(T:Teacher) = {
  	neg find teachesMoreClasses(_Tx,T); // there is noone who would teach more courses
  }


Counting can be used to refer to the size of the match set inside a query. This comes handy e.g. when you want to identify the model element with the minimum/maximum of something. Similarly to neg, cound find involves (a) running (unbound) variable(s) that act(s) as the objects (that)/(whose combinations) will be counted.


Step 6 - disjunction, transitive closure

// Step 6: disjunction, transitive closure
 
   /*
    * Student S1 is friendly to student S2, i.e. they are friends in either direction
    * 
    */
   @QueryExplorer(checked = false)
   pattern friendlyTo(S1:Student, S2:Student) = {
   	Student.friendsWith(S1,S2);
   } or {
   	Student.friendsWith(S2,S1);
   }
 
   /*
    *  Student Someone is in the circle of student S1's friends:
    *  - they are either friendly directly
    *  - or there is a common friend who is in both student's circle of friends
    * 
    * Student S1 is transitively friendly to student Someone
    */
   pattern inTheCircleOfFriends(S1:Student,Someone:Student) = {
   	find friendlyTo+(S1,Someone);
   	S1!=Someone; // we do not allow self loops
   }
 
   /*
    * Student S1 has more transitive friends than student S2
    */
   @QueryExplorer(checked = false)
   pattern moreFriendsThan(S1 : Student, S2 : Student) {
	N == count find inTheCircleOfFriends(S1, _Sx1);
	M == count find inTheCircleOfFriends(S2, _Sx2);
        check(N > M);
   }
 
   /*
    * Student S has the largest circle of friends.
    * 
    * Note: again, there can be multiple matches in case there are
    * equally large friend circles.
    */
   pattern theOnesWithTheBiggestCircle(S:Student) = {
   	neg find moreFriendsThan(Sx,S);
   }


By default, all clauses inside the pattern body are interpreted as a conjunction (i.e. AND). VIATRA Query also supports disjunction by OR patterns, whereby multiple pattern bodies can be assigned.

VIATRA Query includes a specific language construct to define transitive closures, i.e. the computation of reachability relationships where a single step is defined by a (binary) pattern (friendlyTo in the example). This construct is supported by a much more efficient evaluation back-end (evaluated for a recent ICGT conference paper). In order to try the transitive closure, we suggest you to edit the students inside the model, add/remove friendsWith relationships and observe how the match set of the inTheCircleOfFriends patterns change!


Step 7 - combining everything

// Step 7: combine everything
 
   /*
    * 
    * We want to find those years, which had courses taught by the busiest teacher
    * and included the most sociable students
    * 
    */
    @QueryExplorer(message="The busiest teacher $T.name$ taught the most sociable student $S.name$ in $Y.startingDate$")
    @ObservableValue(name = "Year", expression="Y.startingDate")
    @ObservableValue(name = "Teacher", expression="T.name")
    @ObservableValue(name = "Student", expression="S.name")
    pattern finalPattern(Y:Year,C:Course,T:Teacher,S:Student) = {
    	Year.schoolClasses.courses(Y,C);
    	Course.teacher(C,T);
    	Student.schoolClass.courses(S,C);
 
    	find theOnesWithTheBiggestCircle(S);
    	find teachesTheMostCourses(T);
    }


In the final step of the walkthrough, we combine the previously defined patterns to define a multi-parameter query. It is important to note that in VIATRA Query, patterns can have an arbitrary number of parameters, any of which can be bound/unbound freely. The Query Explorer supports parameterized queries (parameter binding) through the Details/Filters panel on the right: by selecting a pattern in the tree, you have the ability to specify Filters for each parameter separately.

  • as pattern parameters can be scalars, such values can be entered by typing the string value or number, boolean literal etc.
  • object pattern parameters can be specified by the model element picker cell editor.

The finalPattern example is intended to highlight the most important advantage of the VIATRA Query pattern language over other (EMF) query languages: it has been designed to make the specification of complex graph queries involving multiple objects and inter-relationships compact and easy-to-understand.

This example also demonstrates the usage of VIATRA Query add-ons by query annotations:

  • @QueryExplorer allows the developer to customize the appearance of a pattern match in the Query Explorer tree. Inside the message, simple path expressions (only allowing direct EAttribute access) can be used inside $$ sections to include match-specific details.
  • @ObservableValue allows the developer to customize the appearance of a match inside the Details panel. It defines an observable value (in the JFace databinding sense) which can be bound to an Eclipse/JFace UI. This annotation will also trigger the generation of an additional .databinding side-project next to your VIATRA Query project, which includes some helper glue code that you can use to ease the integration of VIATRA Query into your application.
  • @Constraint is used to specify VIATRA Query-based well-formedness rules and is discussed in a separate example page: BPMN Validation.

Generated code overview

The source code generated by the VIATRA Query generator is inside the src-gen folder of your VIATRA Query project. It is described on this wiki page in detail.

Back to the top