Repository: cayleygraph/cayley
Pull request #927 (2020-03-21 15:40:00)

LinkedQL: Use data files as test cases instead of code
This PR's goals are:
 - Reduce future boilerplate for writing more extensive tests for LinkedQL
 - Make LinkedQL test cases sharable for other purposes
 - Make sure queries are unmarshalled correctly

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/cayleygraph/cayley/927)
<!-- Reviewable:end -->



Patch:
diff --git a/query/linkedql/steps/jsonld_util.go b/query/linkedql/steps/jsonld_util.go
new file mode 100644
index 0000000..b0e5aa1
--- /dev/null
+++ b/query/linkedql/steps/jsonld_util.go
@@ -0,0 +1,70 @@
+package steps
+
+import (
+	"fmt"
+)
+
+type ldArray = []interface{}
+type ldMap = map[string]interface{}
+
+func unwrapValue(i interface{}) interface{} {
+	m, ok := i.(map[string]interface{})
+	if ok && len(m) == 1 {
+		v, ok := m["@value"]
+		if ok {
+			return v
+		}
+	}
+	return i
+}
+
+// isomorphic checks whether source and target JSON-LD structures are the same
+// semantically. This function is not complete and is maintained for testing
+// purposes. Hopefully in the future it can be proven sufficient for general
+// purpose use.
+func isomorphic(source interface{}, target interface{}) error {
+	source = unwrapValue(source)
+	target = unwrapValue(target)
+	switch s := source.(type) {
+	case string:
+		t, ok := target.(string)
+		if !ok {
+			return fmt.Errorf("Expected %v to be a string but instead received %T", target, target)
+		}
+		if s != t {
+			return fmt.Errorf("Expected \"%v\" but instead received \"%v\"", s, t)
+		}
+		return nil
+	case ldArray:
+		t, ok := target.(ldArray)
+		if !ok {
+			return fmt.Errorf("Expected %#v to be an array but instead received %T", target, target)
+		}
+		if len(s) != len(t) {
+			return fmt.Errorf("Expected %#v and %#v to have the same length", s, t)
+		}
+	items:
+		for _, i := range s {
+			for _, tI := range t {
+				if isomorphic(i, tI) == nil {
+					continue items
+				}
+			}
+			return fmt.Errorf("No matching values for the item %#v in %#v", i, t)
+		}
+		return nil
+	case ldMap:
+		t, ok := target.(ldMap)
+		if !ok {
+			return fmt.Errorf("Expected %#v to be a map but instead received %T", target, target)
+		}
+		for k, v := range s {
+			tV, _ := t[k]
+			err := isomorphic(v, tV)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}


Test patch:
diff --git a/query/linkedql/steps/jsonld_util_test.go b/query/linkedql/steps/jsonld_util_test.go
new file mode 100644
index 0000000..98e0a49
--- /dev/null
+++ b/query/linkedql/steps/jsonld_util_test.go
@@ -0,0 +1,124 @@
+package steps
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+var testCases = []struct {
+	name     string
+	source   interface{}
+	target   interface{}
+	expected error
+}{
+	{
+		name:     "Single matching IDs",
+		source:   map[string]interface{}{"@id": "a"},
+		target:   map[string]interface{}{"@id": "a"},
+		expected: nil,
+	},
+	{
+		name:     "Single non matching IDs",
+		source:   map[string]interface{}{"@id": "a"},
+		target:   map[string]interface{}{"@id": "b"},
+		expected: fmt.Errorf("Expected \"a\" but instead received \"b\""),
+	},
+	{
+		name:     "Single matching properties",
+		source:   map[string]interface{}{"http://example.com/name": "Alice"},
+		target:   map[string]interface{}{"http://example.com/name": "Alice"},
+		expected: nil,
+	},
+	{
+		name:     "Single non matching properties",
+		source:   map[string]interface{}{"http://example.com/name": "Alice"},
+		target:   map[string]interface{}{"http://example.com/name": "Bob"},
+		expected: fmt.Errorf("Expected \"Alice\" but instead received \"Bob\""),
+	},
+	{
+		name:     "Single matching property with multiple values ordered",
+		source:   map[string]interface{}{"http://example.com/name": []interface{}{"Alice", "Bob"}},
+		target:   map[string]interface{}{"http://example.com/name": []interface{}{"Alice", "Bob"}},
+		expected: nil,
+	},
+	{
+		name:     "Single matching property with multiple values unordered",
+		source:   map[string]interface{}{"http://example.com/name": []interface{}{"Alice", "Bob"}},
+		target:   map[string]interface{}{"http://example.com/name": []interface{}{"Bob", "Alice"}},
+		expected: nil,
+	},
+	{
+		name:     "Single non matching property with multiple values",
+		source:   map[string]interface{}{"http://example.com/name": []interface{}{"Alice", "Bob"}},
+		target:   map[string]interface{}{"http://example.com/name": []interface{}{"Dan", "Alice"}},
+		expected: fmt.Errorf("No matching values for the item \"Bob\" in []interface {}{\"Dan\", \"Alice\"}"),
+	},
+	{
+		name:     "Single non matching property with multiple values non matching length",
+		source:   map[string]interface{}{"http://example.com/name": []interface{}{"Alice", "Bob"}},
+		target:   map[string]interface{}{"http://example.com/name": []interface{}{"Alice"}},
+		expected: fmt.Errorf("Expected []interface {}{\"Alice\", \"Bob\"} and []interface {}{\"Alice\"} to have the same length"),
+	},
+	{
+		name: "Single matching nested",
+		source: map[string]interface{}{
+			"http://example.com/friend": map[string]interface{}{
+				"@id": "alice",
+			},
+		},
+		target: map[string]interface{}{
+			"http://example.com/friend": map[string]interface{}{
+				"@id": "alice",
+			},
+		},
+		expected: nil,
+	},
+	{
+		name: "Single non matching nested",
+		source: map[string]interface{}{
+			"http://example.com/friend": map[string]interface{}{
+				"@id": "alice",
+			},
+		},
+		target: map[string]interface{}{
+			"http://example.com/friend": map[string]interface{}{
+				"@id": "bob",
+			},
+		},
+		expected: fmt.Errorf("Expected \"alice\" but instead received \"bob\""),
+	},
+	{
+		name:     "Single matching properties with @value string",
+		source:   map[string]interface{}{"http://example.com/name": map[string]interface{}{"@value": "Alice"}},
+		target:   map[string]interface{}{"http://example.com/name": map[string]interface{}{"@value": "Alice"}},
+		expected: nil,
+	},
+	{
+		name:     "Single non matching properties with @value string",
+		source:   map[string]interface{}{"http://example.com/name": map[string]interface{}{"@value": "Alice"}},
+		target:   map[string]interface{}{"http://example.com/name": map[string]interface{}{"@value": "Bob"}},
+		expected: fmt.Errorf("Expected \"Alice\" but instead received \"Bob\""),
+	},
+	{
+		name:     "Single matching properties with @value string and string",
+		source:   map[string]interface{}{"http://example.com/name": map[string]interface{}{"@value": "Alice"}},
+		target:   map[string]interface{}{"http://example.com/name": "Alice"},
+		expected: nil,
+	},
+	{
+		name:     "Single matching properties with string and @value string",
+		source:   map[string]interface{}{"http://example.com/name": "Alice"},
+		target:   map[string]interface{}{"http://example.com/name": map[string]interface{}{"@value": "Alice"}},
+		expected: nil,
+	},
+}
+
+func TestIsomorphic(t *testing.T) {
+	for _, c := range testCases {
+		t.Run(c.name, func(t *testing.T) {
+			require.Equal(t, c.expected, isomorphic(c.source, c.target))
+		})
+	}
+}
diff --git a/query/linkedql/steps/steps_test.go b/query/linkedql/steps/steps_test.go
index eeb4d48..0f68601 100644
--- a/query/linkedql/steps/steps_test.go
+++ b/query/linkedql/steps/steps_test.go
@@ -1,634 +1,97 @@
 package steps
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/cayleygraph/cayley/graph/memstore"
 	"github.com/cayleygraph/cayley/query/linkedql"
 	"github.com/cayleygraph/quad"
+	"github.com/cayleygraph/quad/jsonld"
 	"github.com/cayleygraph/quad/voc"
-	"github.com/cayleygraph/quad/voc/xsd"
 	"github.com/stretchr/testify/require"
 )
 
-var (
-	exNS      = "http://example.org/"
-	alice     = quad.IRI(exNS + "alice")
-	bob       = quad.IRI(exNS + "bob")
-	dan       = quad.IRI(exNS + "dan")
-	likes     = quad.IRI(exNS + "likes")
-	likesName = quad.IRI(exNS + "likesName")
-	name      = quad.IRI(exNS + "name")
-	person    = quad.IRI(exNS + "person")
-	liker     = quad.IRI(exNS + "liker")
-	liked     = quad.IRI(exNS + "liked")
-)
-
-func newQuad(subject quad.Value, predicate quad.Value, object quad.Value, label quad.Value) quad.Quad {
-	return quad.Quad{Subject: subject, Predicate: predicate, Object: object, Label: label}
+type TestCase struct {
+	Data    interface{} `json:"data"`
+	Query   interface{} `json:"query"`
+	Results interface{} `json:"results"`
 }
 
-var singleQuadData = []quad.Quad{
-	newQuad(alice, likes, bob, nil),
+func readData(data interface{}) ([]quad.Quad, error) {
+	d, err := json.Marshal(data)
+	if err != nil {
+		return nil, err
+	}
+	b := bytes.NewBuffer(d)
+	reader := jsonld.NewReader(b)
+	quads, err := quad.ReadAll(reader)
+	if err != nil {
+		return nil, err
+	}
+	return quads, nil
 }
 
-var testCases = []struct {
-	name    string
-	data    []quad.Quad
-	query   linkedql.IteratorStep
-	results []interface{}
-}{
-	{
-		name: "All Entities",
-		data: singleQuadData,
-		query: &Entities{Identifiers: []linkedql.EntityIdentifier{
-			linkedql.NewEntityIdentifier(linkedql.EntityIdentifierString(alice)),
-		}},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-		},
-	},
-	{
-		name:  "All Vertices",
-		data:  singleQuadData,
-		query: &Vertex{Values: nil},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-			map[string]string{"@id": string(likes)},
-			map[string]string{"@id": string(bob)},
-		},
-	},
-	{
-		name: "Select",
-		data: singleQuadData,
-		query: &Select{
-			From: &Visit{
-				From: &As{
-					From: &Vertex{},
-					Name: string(liker),
-				},
-				Properties: linkedql.NewPropertyPath(linkedql.PropertyIRI(likes)),
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				string(liker): map[string]string{"@id": string(alice)},
-			},
-		},
-	},
-	{
-		name: "Select with tags",
-		data: singleQuadData,
-		query: &Select{
-			Tags: []string{string(liker)},
-			From: &As{
-				From: &Visit{
-					From: &As{
-						From: &Vertex{},
-						Name: string(liker),
-					},
-					Properties: linkedql.NewPropertyPath(linkedql.PropertyIRI(likes)),
-				},
-				Name: "liked",
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				string(liker): map[string]string{"@id": string(alice)},
-			},
-		},
-	},
-	{
-		name: "Back",
-		data: singleQuadData,
-		query: &Back{
-			From: &Visit{
-				From: &Vertex{
-					Values: []quad.Value{alice},
-				},
-				Properties: linkedql.NewPropertyPath(&Vertex{
-					Values: []quad.Value{
-						likes,
-					},
-				}),
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-		},
-	},
-	{
-		name: "Both",
-		data: []quad.Quad{
-			newQuad(dan, likes, bob, nil),
-			newQuad(bob, likes, alice, nil),
-		},
-		query: &Both{
-			From: &Vertex{
-				Values: []quad.Value{bob},
-			},
-			Properties: linkedql.NewPropertyPath(linkedql.PropertyIRI(likes)),
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(dan)},
-			map[string]string{"@id": string(alice)},
-		},
-	},
-	{
-		name: "Count",
-		data: singleQuadData,
-		query: &Count{
-			From: &Vertex{Values: []quad.Value{}},
-		},
-		results: []interface{}{
-			map[string]string{"@value": "4", "@type": xsd.Integer},
-		},
-	},
-	{
-		name: "Difference",
-		data: singleQuadData,
-		query: &Difference{
-			From: &Vertex{
-				Values: []quad.Value{alice, likes},
-			},
-			Steps: []linkedql.PathStep{
-				&Vertex{
-					Values: []quad.Value{likes},
-				},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-		},
-	},
-	{
-		name: "RegExp",
-		data: []quad.Quad{
-			{Subject: alice, Predicate: name, Object: quad.String("Alice"), Label: nil},
-		},
-		query: &RegExp{Expression: "A", From: &Vertex{Values: []quad.Value{}}},
-		results: []interface{}{
-			"Alice",
-		},
-	},
-	{
-		name: "Like",
-		data: []quad.Quad{
-			{Subject: alice, Predicate: name, Object: quad.String("Alice"), Label: nil},
-		},
-		query: &Like{Pattern: "%ali%", From: &Vertex{Values: []quad.Value{}}},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-		},
-	},
-	{
-		name: "Filter LessThan",
-		data: []quad.Quad{
-			{Subject: alice, Predicate: name, Object: quad.Int(0), Label: nil},
-			{Subject: alice, Predicate: name, Object: quad.Int(1), Label: nil},
-		},
-		query: &LessThan{
-			From:  &Vertex{Values: []quad.Value{}},
-			Value: quad.Int(1),
-		},
-		results: []interface{}{
-			map[string]string{"@value": "0", "@type": "xsd:integer"},
-		},
-	},
-	{
-		name: "Filter GreaterThan",
-		data: []quad.Quad{
-			{Subject: alice, Predicate: name, Object: quad.Int(0), Label: nil},
-			{Subject: alice, Predicate: name, Object: quad.Int(1), Label: nil},
-		},
-		query: &GreaterThan{
-			From:  &Vertex{Values: []quad.Value{}},
-			Value: quad.Int(0),
-		},
-		results: []interface{}{
-			map[string]string{"@value": "1", "@type": "xsd:integer"},
-		},
-	},
-	{
-		name: "Filter LessThanEquals",
-		data: []quad.Quad{
-			{Subject: alice, Predicate: name, Object: quad.Int(-1), Label: nil},
-			{Subject: alice, Predicate: name, Object: quad.Int(0), Label: nil},
-			{Subject: alice, Predicate: name, Object: quad.Int(1), Label: nil},
-		},
-		query: &LessThanEquals{
-			From:  &Vertex{Values: []quad.Value{}},
-			Value: quad.Int(0),
-		},
-		results: []interface{}{
-			map[string]string{"@value": "-1", "@type": "xsd:integer"},
-			map[string]string{"@value": "0", "@type": "xsd:integer"},
-		},
-	},
-	{
-		name: "Filter GreaterThanEquals",
-		data: []quad.Quad{
-			{Subject: alice, Predicate: name, Object: quad.Int(0), Label: nil},
-			{Subject: alice, Predicate: name, Object: quad.Int(1), Label: nil},
-			{Subject: alice, Predicate: name, Object: quad.Int(2), Label: nil},
-		},
-		query: &GreaterThanEquals{
-			From:  &Vertex{Values: []quad.Value{}},
-			Value: quad.Int(1),
-		},
-		results: []interface{}{
-			map[string]string{"@value": "1", "@type": "xsd:integer"},
-			map[string]string{"@value": "2", "@type": "xsd:integer"},
-		},
-	},
-	{
-		name: "Has",
-		data: singleQuadData,
-		query: &Has{
-			From: &Vertex{
-				Values: []quad.Value{},
-			},
-			Property: linkedql.NewPropertyPath(&Vertex{
-				Values: []quad.Value{likes},
-			}),
-			Values: []quad.Value{bob},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-		},
-	},
-	{
-		name: "HasReverse",
-		data: singleQuadData,
-		query: &HasReverse{
-			From: &Vertex{
-				Values: []quad.Value{},
-			},
-			Property: linkedql.NewPropertyPath(&Vertex{
-				Values: []quad.Value{likes},
-			}),
-			Values: []quad.Value{alice},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(bob)},
-		},
-	},
-	{
-		name: "ViewReverse",
-		data: singleQuadData,
-		query: &VisitReverse{
-			From:       &Vertex{Values: []quad.Value{}},
-			Properties: linkedql.NewPropertyPath(linkedql.PropertyIRI(likes)),
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-		},
-	},
-	{
-		name: "PropertyNames",
-		data: singleQuadData,
-		query: &PropertyNames{
-			From: &Vertex{Values: []quad.Value{}},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(likes)},
-		},
-	},
-	{
-		name: "Intersect",
-		data: []quad.Quad{
-			newQuad(bob, likes, alice, nil),
-			newQuad(dan, likes, alice, nil),
-		},
-		query: &Intersect{
-			From: &Visit{
-				From: &Vertex{Values: []quad.Value{bob}},
-				Properties: linkedql.NewPropertyPath(&Vertex{
-					Values: []quad.Value{likes},
-				}),
-			},
-			Steps: []linkedql.PathStep{
-				&Visit{
-					From:       &Vertex{Values: []quad.Value{bob}},
-					Properties: linkedql.NewPropertyPath(linkedql.PropertyIRI(likes)),
-				},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-		},
-	},
-	{
-		name: "Is",
-		data: singleQuadData,
-		query: &Is{
-			Values: []quad.Value{bob},
-			From: &Visit{
-				From: &Vertex{Values: []quad.Value{alice}},
-				Properties: linkedql.NewPropertyPath(&Vertex{
-					Values: []quad.Value{likes},
-				}),
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(bob)},
-		},
-	},
-	{
-		name: "Limit",
-		data: singleQuadData,
-		query: &Limit{
-			Limit: 2,
-			From: &Vertex{
-				Values: []quad.Value{},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-			map[string]string{"@id": string(likes)},
-		},
-	},
-	{
-		name: "View",
-		data: singleQuadData,
-		query: &Visit{
-			From:       &Vertex{Values: []quad.Value{}},
-			Properties: linkedql.NewPropertyPath(linkedql.PropertyIRI(likes)),
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(bob)},
-		},
-	},
-	{
-		name: "PropertyNames",
-		data: singleQuadData,
-		query: &PropertyNames{
-			From: &Vertex{Values: []quad.Value{}},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(likes)},
-		},
-	},
-	{
-		name: "Properties",
-		data: singleQuadData,
-		query: &Select{
-			From: &Properties{
-				From:  &Vertex{Values: []quad.Value{}},
-				Names: linkedql.NewPropertyPath(linkedql.PropertyIRI(likes)),
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				string(likes): map[string]string{"@id": string(bob)},
-			},
-		},
-	},
-	{
-		name: "ReversePropertyNamesAs",
-		data: singleQuadData,
-		query: &Select{
-			From: &ReversePropertyNamesAs{
-				From: &Vertex{Values: []quad.Value{}},
-				Tag:  "property",
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				"property": map[string]string{"@id": string(likes)},
-			},
-		},
-	},
-	{
-		name: "PropertyNamesAs",
-		data: singleQuadData,
-		query: &Select{
-			From: &PropertyNamesAs{
-				From: &Vertex{Values: []quad.Value{}},
-				Tag:  "property",
-			},
-		},
-		results: []interface{}{map[string]interface{}{
-			"property": map[string]string{"@id": string(likes)},
-		}},
-	},
-	{
-		name: "ReverseProperties",
-		data: singleQuadData,
-		query: &Select{
-			From: &ReverseProperties{
-				From:  &Vertex{Values: []quad.Value{}},
-				Names: linkedql.NewPropertyPath(linkedql.PropertyIRI(likes)),
-			},
-		},
-		results: []interface{}{map[string]interface{}{
-			string(likes): map[string]string{"@id": string(alice)},
-		}},
-	},
-	{
-		name: "Skip",
-		data: singleQuadData,
-		query: &Skip{
-			Offset: 2,
-			From: &Vertex{
-				Values: []quad.Value{},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(bob)},
-		},
-	},
-	{
-		name: "Union",
-		data: singleQuadData,
-		query: &Union{
-			From: &Vertex{
-				Values: []quad.Value{alice},
-			},
-			Steps: []linkedql.PathStep{
-				&Vertex{
-					Values: []quad.Value{bob},
-				},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-			map[string]string{"@id": string(bob)},
-		},
-	},
-	{
-		name: "SelectFirst",
-		data: singleQuadData,
-		query: &SelectFirst{
-			From: &Visit{
-				From: &As{
-					Name: string(liker),
-					From: &Vertex{},
-				},
-				Properties: linkedql.NewPropertyPath(linkedql.PropertyIRI(likes)),
-			},
-		},
-		results: []interface{}{map[string]interface{}{
-			string(liker): map[string]string{"@id": string(alice)},
-		}},
-	},
-	{
-		name: "Unique",
-		data: singleQuadData,
-		query: &Unique{
-			From: &Vertex{
-				Values: []quad.Value{alice, alice, bob},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-			map[string]string{"@id": string(bob)},
-		},
-	},
-	{
-		name: "Order",
-		data: singleQuadData,
-		query: &Order{
-			From: &Vertex{},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-			map[string]string{"@id": string(bob)},
-			map[string]string{"@id": string(likes)},
-		},
-	},
-	{
-		name: "Optional",
-		data: []quad.Quad{
-			newQuad(alice, likes, bob, nil),
-			newQuad(alice, name, quad.String("Alice"), nil),
-			newQuad(bob, name, quad.String("Bob"), nil),
-		},
-		query: &Select{
-			From: &Optional{
-				From: &Properties{
-					From:  &Vertex{Values: []quad.Value{}},
-					Names: linkedql.NewPropertyPath(linkedql.PropertyIRI(name)),
-				},
-				Step: &Properties{
-					From:  &Placeholder{},
-					Names: linkedql.NewPropertyPath(linkedql.PropertyIRI(likes)),
-				},
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				string(likes): map[string]string{"@id": string(bob)},
-				string(name):  "Alice",
-			},
-			map[string]interface{}{
-				string(name): "Bob",
-			},
-		},
-	},
-	{
-		name: "Where",
-		data: []quad.Quad{
-			newQuad(alice, likes, bob, nil),
-			newQuad(alice, name, quad.String("Alice"), nil),
-			newQuad(bob, name, quad.String("Bob"), nil),
-		},
-		query: &Select{
-			From: &Where{
-				From: &Vertex{},
-				Steps: []linkedql.PathStep{
-					&As{
-						From: &Visit{
-							From: &Visit{
-								From:       &Placeholder{},
-								Properties: linkedql.NewPropertyPath(linkedql.PropertyIRI(likes)),
-							},
-							Properties: linkedql.NewPropertyPath(linkedql.PropertyIRI(name)),
-						},
-						Name: string(likesName),
-					},
-					&As{
-						From: &Visit{
-							From:       &Placeholder{},
-							Properties: linkedql.NewPropertyPath(linkedql.PropertyIRI(name)),
-						},
-						Name: string(name),
-					},
-				},
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				string(name):      "Alice",
-				string(likesName): "Bob",
-			},
-		},
-	},
-	{
-		name: "Documents",
-		data: []quad.Quad{
-			newQuad(alice, likes, bob, nil),
-			newQuad(alice, name, quad.String("Alice"), nil),
-			newQuad(bob, name, quad.String("Bob"), nil),
-			newQuad(bob, likes, alice, nil),
-		},
-		query: &Documents{
-			From: &Properties{
-				From:  &Vertex{Values: []quad.Value{}},
-				Names: linkedql.NewPropertyPath(linkedql.PropertyIRIs{linkedql.PropertyIRI(name), linkedql.PropertyIRI(likes)}),
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				"@id":         string(alice),
-				string(name):  []interface{}{"Alice"},
-				string(likes): []interface{}{map[string]string{"@id": string(bob)}},
-			},
-			map[string]interface{}{
-				"@id":         string(bob),
-				string(name):  []interface{}{"Bob"},
-				string(likes): []interface{}{map[string]string{"@id": string(alice)}},
-			},
-		},
-	},
-	{
-		name: "Context",
-		data: []quad.Quad{
-			newQuad(alice, likes, bob, nil),
-			newQuad(bob, likes, alice, nil),
-		},
-		query: &Context{
-			From: &Has{
-				From:     &Vertex{},
-				Property: linkedql.NewPropertyPath(linkedql.PropertyIRI("likes")),
-				Values:   []quad.Value{bob},
-			},
-			Rules: map[string]string{
-				"bob":   string(bob),
-				"likes": string(likes),
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": string(alice)},
-		},
-	},
+func readQuery(raw interface{}) (linkedql.IteratorStep, error) {
+	d, err := json.Marshal(raw)
+	if err != nil {
+		return nil, err
+	}
+	q, err := linkedql.Unmarshal(d)
+	if err != nil {
+		return nil, err
+	}
+	query, ok := q.(linkedql.IteratorStep)
+	if !ok {
+		return nil, fmt.Errorf("Expected linkedql.IteratorStep")
+	}
+	return query, nil
 }
 
 func TestLinkedQL(t *testing.T) {
-	for _, c := range testCases {
-		t.Run(c.name, func(t *testing.T) {
-			store := memstore.New(c.data...)
+	// Using files
+	direcotry := "test-cases"
+	files, err := ioutil.ReadDir(direcotry)
+	if err != nil {
+		require.NoError(t, err)
+	}
+	for _, info := range files {
+		fileName := info.Name()
+		filePath := filepath.Join(direcotry, fileName)
+		file, err := ioutil.ReadFile(filePath)
+		require.NoError(t, err)
+
+		var c TestCase
+		err = json.Unmarshal(file, &c)
+		require.NoError(t, err)
+
+		data, err := readData(c.Data)
+		require.NoError(t, err, fileName)
+		require.NotEmpty(t, data, fileName)
+
+		query, err := readQuery(c.Query)
+		require.NoError(t, err, fileName)
+		require.NotNil(t, query, fileName)
+
+		testName := strings.TrimSuffix(fileName, filepath.Ext(fileName))
+		t.Run(testName, func(t *testing.T) {
+			store := memstore.New(data...)
 			voc := voc.Namespaces{}
 			ctx := context.TODO()
-			iterator, err := c.query.BuildIterator(store, &voc)
+			iterator, err := query.BuildIterator(store, &voc)
 			require.NoError(t, err)
 			var results []interface{}
 			for iterator.Next(ctx) {
 				results = append(results, iterator.Result())
 			}
 			require.NoError(t, iterator.Err())
-			require.Equal(t, c.results, results)
+			require.Equal(t, nil, isomorphic(c.Results, results))
 		})
 	}
 }
diff --git a/query/linkedql/steps/test-cases/Back.json b/query/linkedql/steps/test-cases/Back.json
new file mode 100644
index 0000000..a2bf7d6
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Back.json
@@ -0,0 +1,36 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Back",
+    "linkedql:from": {
+      "@type": "linkedql:Visit",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [
+          {
+            "@id": "http://example.com/alice"
+          }
+        ]
+      },
+      "linkedql:properties": ["http://example.com/likes"]
+    },
+    "linkedql:name": ""
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Both.json b/query/linkedql/steps/test-cases/Both.json
new file mode 100644
index 0000000..854ec4b
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Both.json
@@ -0,0 +1,39 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ]
+    },
+    {
+      "@id": "http://example.com/dan",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Both",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": [{ "@id": "http://example.com/bob" }]
+    },
+    "linkedql:properties": "http://example.com/likes"
+  },
+  "results": [
+    {
+      "@id": "http://example.com/dan"
+    },
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Context.json b/query/linkedql/steps/test-cases/Context.json
new file mode 100644
index 0000000..69e480b
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Context.json
@@ -0,0 +1,44 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    },
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Context",
+    "linkedql:from": {
+      "@type": "linkedql:Has",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      },
+      "linkedql:property": "likes",
+      "linkedql:values": [{ "@id": "http://example.com/bob" }]
+    },
+    "linkedql:rules": {
+      "bob": "http://example.com/bob",
+      "likes": "http://example.com/likes"
+    }
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Count.json b/query/linkedql/steps/test-cases/Count.json
new file mode 100644
index 0000000..0637bac
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Count.json
@@ -0,0 +1,23 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Count",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    }
+  },
+  "results": [4]
+}
diff --git a/query/linkedql/steps/test-cases/Difference.json b/query/linkedql/steps/test-cases/Difference.json
new file mode 100644
index 0000000..f195cad
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Difference.json
@@ -0,0 +1,40 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Difference",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": [
+        { "@id": "http://example.com/alice" },
+        { "@id": "http://example.com/likes" }
+      ]
+    },
+    "linkedql:steps": [
+      {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [
+          {
+            "@id": "http://example.com/likes"
+          }
+        ]
+      }
+    ]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Documents.json b/query/linkedql/steps/test-cases/Documents.json
new file mode 100644
index 0000000..f4ad1c3
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Documents.json
@@ -0,0 +1,56 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ],
+      "http://example.com/name": ["Alice"]
+    },
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ],
+      "http://example.com/name": ["Bob"]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Documents",
+    "linkedql:from": {
+      "@type": "linkedql:Properties",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      },
+      "linkedql:names": ["http://example.com/name", "http://example.com/likes"]
+    }
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ],
+      "http://example.com/name": ["Alice"]
+    },
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ],
+      "http://example.com/name": ["Bob"]
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Has.json b/query/linkedql/steps/test-cases/Has.json
new file mode 100644
index 0000000..538bf5f
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Has.json
@@ -0,0 +1,33 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Has",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:property": "http://example.com/likes",
+    "linkedql:values": [
+      {
+        "@id": "http://example.com/bob"
+      }
+    ]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Intersect.json b/query/linkedql/steps/test-cases/Intersect.json
new file mode 100644
index 0000000..b1600a3
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Intersect.json
@@ -0,0 +1,57 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ]
+    },
+    {
+      "@id": "http://example.com/dan",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Intersect",
+    "linkedql:from": {
+      "@type": "linkedql:Visit",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [
+          {
+            "@id": "http://example.com/bob"
+          }
+        ]
+      },
+      "linkedql:properties": "http://example.com/likes"
+    },
+    "linkedql:steps": [
+      {
+        "@type": "linkedql:Visit",
+        "linkedql:from": {
+          "@type": "linkedql:Vertex",
+          "linkedql:values": [
+            {
+              "@id": "http://example.com/bob"
+            }
+          ]
+        },
+        "linkedql:properties": "http://example.com/likes"
+      }
+    ]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Is.json b/query/linkedql/steps/test-cases/Is.json
new file mode 100644
index 0000000..da40136
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Is.json
@@ -0,0 +1,35 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Is",
+    "linkedql:from": {
+      "@type": "linkedql:Visit",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [{ "@id": "http://example.com/alice" }]
+      },
+      "linkedql:properties": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [{ "@id": "http://example.com/likes" }]
+      }
+    },
+    "linkedql:values": [{ "@id": "http://example.com/bob" }]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Like.json b/query/linkedql/steps/test-cases/Like.json
new file mode 100644
index 0000000..babb76b
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Like.json
@@ -0,0 +1,24 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/name": ["Alice"]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Like",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:likePattern": "%ali%"
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Limit.json b/query/linkedql/steps/test-cases/Limit.json
new file mode 100644
index 0000000..4ef08d1
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Limit.json
@@ -0,0 +1,31 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Limit",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:limit": 2
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    },
+    {
+      "@id": "http://example.com/likes"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Optional.json b/query/linkedql/steps/test-cases/Optional.json
new file mode 100644
index 0000000..4a888d5
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Optional.json
@@ -0,0 +1,53 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ],
+      "http://example.com/name": ["Alice"]
+    },
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/name": ["Bob"]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:Optional",
+      "linkedql:from": {
+        "@type": "linkedql:Properties",
+        "linkedql:from": {
+          "@type": "linkedql:Vertex",
+          "linkedql:values": []
+        },
+        "linkedql:names": ["http://example.com/name"]
+      },
+      "linkedql:step": {
+        "@type": "linkedql:Properties",
+        "linkedql:from": {
+          "@type": "linkedql:Placeholder"
+        },
+        "linkedql:names": ["http://example.com/likes"]
+      }
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "http://example.com/likes": {
+        "@id": "http://example.com/bob"
+      },
+      "http://example.com/name": "Alice"
+    },
+    {
+      "http://example.com/name": "Bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Order.json b/query/linkedql/steps/test-cases/Order.json
new file mode 100644
index 0000000..0afe648
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Order.json
@@ -0,0 +1,33 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Order",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    }
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    },
+    {
+      "@id": "http://example.com/bob"
+    },
+    {
+      "@id": "http://example.com/likes"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Properties.json b/query/linkedql/steps/test-cases/Properties.json
new file mode 100644
index 0000000..b845f4c
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Properties.json
@@ -0,0 +1,34 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:Properties",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      },
+      "linkedql:names": ["http://example.com/likes"]
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "http://example.com/likes": {
+        "@id": "http://example.com/bob"
+      }
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Select.json b/query/linkedql/steps/test-cases/Select.json
new file mode 100644
index 0000000..81cb3f9
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Select.json
@@ -0,0 +1,38 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:Visit",
+      "linkedql:from": {
+        "@type": "linkedql:As",
+        "linkedql:from": {
+          "@type": "linkedql:Vertex",
+          "linkedql:values": []
+        },
+        "linkedql:name": "http://example.com/liker"
+      },
+      "linkedql:properties": "http://example.com/likes"
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "http://example.com/liker": {
+        "@id": "http://example.com/alice"
+      }
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Skip.json b/query/linkedql/steps/test-cases/Skip.json
new file mode 100644
index 0000000..641089a
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Skip.json
@@ -0,0 +1,28 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Skip",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:offset": 2
+  },
+  "results": [
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Union.json b/query/linkedql/steps/test-cases/Union.json
new file mode 100644
index 0000000..857abb7
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Union.json
@@ -0,0 +1,44 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Union",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ]
+    },
+    "linkedql:steps": [
+      {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [
+          {
+            "@id": "http://example.com/bob"
+          }
+        ]
+      }
+    ]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    },
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Unique.json b/query/linkedql/steps/test-cases/Unique.json
new file mode 100644
index 0000000..4582397
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Unique.json
@@ -0,0 +1,34 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Unique",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": [
+        { "@id": "http://example.com/alice" },
+        { "@id": "http://example.com/alice" },
+        { "@id": "http://example.com/bob" }
+      ]
+    }
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    },
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/View.json b/query/linkedql/steps/test-cases/View.json
new file mode 100644
index 0000000..6177056
--- /dev/null
+++ b/query/linkedql/steps/test-cases/View.json
@@ -0,0 +1,28 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Visit",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:properties": "http://example.com/likes"
+  },
+  "results": [
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/Where.json b/query/linkedql/steps/test-cases/Where.json
new file mode 100644
index 0000000..117c7fd
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Where.json
@@ -0,0 +1,65 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ],
+      "http://example.com/name": ["Alice"]
+    },
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/name": ["Bob"]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:Where",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      },
+      "linkedql:steps": [
+        {
+          "@type": "linkedql:As",
+          "linkedql:from": {
+            "@type": "linkedql:Visit",
+            "linkedql:from": {
+              "@type": "linkedql:Visit",
+              "linkedql:from": {
+                "@type": "linkedql:Placeholder"
+              },
+              "linkedql:properties": "http://example.com/likes"
+            },
+            "linkedql:properties": "http://example.com/name"
+          },
+          "linkedql:name": "http://example.com/likesName"
+        },
+        {
+          "@type": "linkedql:As",
+          "linkedql:from": {
+            "@type": "linkedql:Visit",
+            "linkedql:from": {
+              "@type": "linkedql:Placeholder"
+            },
+            "linkedql:properties": "http://example.com/name"
+          },
+          "linkedql:name": "http://example.com/name"
+        }
+      ]
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "http://example.com/likesName": "Bob",
+      "http://example.com/name": "Alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/all-entities.json b/query/linkedql/steps/test-cases/all-entities.json
new file mode 100644
index 0000000..e757e6c
--- /dev/null
+++ b/query/linkedql/steps/test-cases/all-entities.json
@@ -0,0 +1,24 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Entities",
+    "linkedql:identifiers": ["http://example.com/alice"]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/all-vertices.json b/query/linkedql/steps/test-cases/all-vertices.json
new file mode 100644
index 0000000..e7730df
--- /dev/null
+++ b/query/linkedql/steps/test-cases/all-vertices.json
@@ -0,0 +1,30 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Vertex",
+    "linkedql:values": []
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    },
+    {
+      "@id": "http://example.com/likes"
+    },
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/back.json b/query/linkedql/steps/test-cases/back.json
new file mode 100644
index 0000000..a2bf7d6
--- /dev/null
+++ b/query/linkedql/steps/test-cases/back.json
@@ -0,0 +1,36 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Back",
+    "linkedql:from": {
+      "@type": "linkedql:Visit",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [
+          {
+            "@id": "http://example.com/alice"
+          }
+        ]
+      },
+      "linkedql:properties": ["http://example.com/likes"]
+    },
+    "linkedql:name": ""
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/both.json b/query/linkedql/steps/test-cases/both.json
new file mode 100644
index 0000000..854ec4b
--- /dev/null
+++ b/query/linkedql/steps/test-cases/both.json
@@ -0,0 +1,39 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ]
+    },
+    {
+      "@id": "http://example.com/dan",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Both",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": [{ "@id": "http://example.com/bob" }]
+    },
+    "linkedql:properties": "http://example.com/likes"
+  },
+  "results": [
+    {
+      "@id": "http://example.com/dan"
+    },
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/context.json b/query/linkedql/steps/test-cases/context.json
new file mode 100644
index 0000000..69e480b
--- /dev/null
+++ b/query/linkedql/steps/test-cases/context.json
@@ -0,0 +1,44 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    },
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Context",
+    "linkedql:from": {
+      "@type": "linkedql:Has",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      },
+      "linkedql:property": "likes",
+      "linkedql:values": [{ "@id": "http://example.com/bob" }]
+    },
+    "linkedql:rules": {
+      "bob": "http://example.com/bob",
+      "likes": "http://example.com/likes"
+    }
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/count.json b/query/linkedql/steps/test-cases/count.json
new file mode 100644
index 0000000..0637bac
--- /dev/null
+++ b/query/linkedql/steps/test-cases/count.json
@@ -0,0 +1,23 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Count",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    }
+  },
+  "results": [4]
+}
diff --git a/query/linkedql/steps/test-cases/difference.json b/query/linkedql/steps/test-cases/difference.json
new file mode 100644
index 0000000..f195cad
--- /dev/null
+++ b/query/linkedql/steps/test-cases/difference.json
@@ -0,0 +1,40 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Difference",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": [
+        { "@id": "http://example.com/alice" },
+        { "@id": "http://example.com/likes" }
+      ]
+    },
+    "linkedql:steps": [
+      {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [
+          {
+            "@id": "http://example.com/likes"
+          }
+        ]
+      }
+    ]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/documents.json b/query/linkedql/steps/test-cases/documents.json
new file mode 100644
index 0000000..f4ad1c3
--- /dev/null
+++ b/query/linkedql/steps/test-cases/documents.json
@@ -0,0 +1,56 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ],
+      "http://example.com/name": ["Alice"]
+    },
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ],
+      "http://example.com/name": ["Bob"]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Documents",
+    "linkedql:from": {
+      "@type": "linkedql:Properties",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      },
+      "linkedql:names": ["http://example.com/name", "http://example.com/likes"]
+    }
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ],
+      "http://example.com/name": ["Alice"]
+    },
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ],
+      "http://example.com/name": ["Bob"]
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/greater-than-equals.json b/query/linkedql/steps/test-cases/greater-than-equals.json
new file mode 100644
index 0000000..f054c4e
--- /dev/null
+++ b/query/linkedql/steps/test-cases/greater-than-equals.json
@@ -0,0 +1,20 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/name": [0, 1, 2]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:GreaterThanEquals",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:value": 1
+  },
+  "results": [1, 2]
+}
diff --git a/query/linkedql/steps/test-cases/greater-than.json b/query/linkedql/steps/test-cases/greater-than.json
new file mode 100644
index 0000000..e0aa32f
--- /dev/null
+++ b/query/linkedql/steps/test-cases/greater-than.json
@@ -0,0 +1,20 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/name": [0, 1]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:GreaterThan",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:value": 0
+  },
+  "results": [1]
+}
diff --git a/query/linkedql/steps/test-cases/has-reverse.json b/query/linkedql/steps/test-cases/has-reverse.json
new file mode 100644
index 0000000..e366c85
--- /dev/null
+++ b/query/linkedql/steps/test-cases/has-reverse.json
@@ -0,0 +1,33 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:HasReverse",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:property": "http://example.com/likes",
+    "linkedql:values": [
+      {
+        "@id": "http://example.com/alice"
+      }
+    ]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/has.json b/query/linkedql/steps/test-cases/has.json
new file mode 100644
index 0000000..538bf5f
--- /dev/null
+++ b/query/linkedql/steps/test-cases/has.json
@@ -0,0 +1,33 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Has",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:property": "http://example.com/likes",
+    "linkedql:values": [
+      {
+        "@id": "http://example.com/bob"
+      }
+    ]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/intersect.json b/query/linkedql/steps/test-cases/intersect.json
new file mode 100644
index 0000000..b1600a3
--- /dev/null
+++ b/query/linkedql/steps/test-cases/intersect.json
@@ -0,0 +1,57 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ]
+    },
+    {
+      "@id": "http://example.com/dan",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Intersect",
+    "linkedql:from": {
+      "@type": "linkedql:Visit",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [
+          {
+            "@id": "http://example.com/bob"
+          }
+        ]
+      },
+      "linkedql:properties": "http://example.com/likes"
+    },
+    "linkedql:steps": [
+      {
+        "@type": "linkedql:Visit",
+        "linkedql:from": {
+          "@type": "linkedql:Vertex",
+          "linkedql:values": [
+            {
+              "@id": "http://example.com/bob"
+            }
+          ]
+        },
+        "linkedql:properties": "http://example.com/likes"
+      }
+    ]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/is.json b/query/linkedql/steps/test-cases/is.json
new file mode 100644
index 0000000..da40136
--- /dev/null
+++ b/query/linkedql/steps/test-cases/is.json
@@ -0,0 +1,35 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Is",
+    "linkedql:from": {
+      "@type": "linkedql:Visit",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [{ "@id": "http://example.com/alice" }]
+      },
+      "linkedql:properties": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [{ "@id": "http://example.com/likes" }]
+      }
+    },
+    "linkedql:values": [{ "@id": "http://example.com/bob" }]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/less-than-equals.json b/query/linkedql/steps/test-cases/less-than-equals.json
new file mode 100644
index 0000000..2dc5355
--- /dev/null
+++ b/query/linkedql/steps/test-cases/less-than-equals.json
@@ -0,0 +1,20 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/name": [-1, 0, 1]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:LessThanEquals",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:value": 0
+  },
+  "results": [-1, 0]
+}
diff --git a/query/linkedql/steps/test-cases/less-than.json b/query/linkedql/steps/test-cases/less-than.json
new file mode 100644
index 0000000..17e662f
--- /dev/null
+++ b/query/linkedql/steps/test-cases/less-than.json
@@ -0,0 +1,20 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/name": [0, 1]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:LessThan",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:value": 1
+  },
+  "results": [0]
+}
diff --git a/query/linkedql/steps/test-cases/like.json b/query/linkedql/steps/test-cases/like.json
new file mode 100644
index 0000000..babb76b
--- /dev/null
+++ b/query/linkedql/steps/test-cases/like.json
@@ -0,0 +1,24 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/name": ["Alice"]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Like",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:likePattern": "%ali%"
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/limit.json b/query/linkedql/steps/test-cases/limit.json
new file mode 100644
index 0000000..4ef08d1
--- /dev/null
+++ b/query/linkedql/steps/test-cases/limit.json
@@ -0,0 +1,31 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Limit",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:limit": 2
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    },
+    {
+      "@id": "http://example.com/likes"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/optional.json b/query/linkedql/steps/test-cases/optional.json
new file mode 100644
index 0000000..4a888d5
--- /dev/null
+++ b/query/linkedql/steps/test-cases/optional.json
@@ -0,0 +1,53 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ],
+      "http://example.com/name": ["Alice"]
+    },
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/name": ["Bob"]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:Optional",
+      "linkedql:from": {
+        "@type": "linkedql:Properties",
+        "linkedql:from": {
+          "@type": "linkedql:Vertex",
+          "linkedql:values": []
+        },
+        "linkedql:names": ["http://example.com/name"]
+      },
+      "linkedql:step": {
+        "@type": "linkedql:Properties",
+        "linkedql:from": {
+          "@type": "linkedql:Placeholder"
+        },
+        "linkedql:names": ["http://example.com/likes"]
+      }
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "http://example.com/likes": {
+        "@id": "http://example.com/bob"
+      },
+      "http://example.com/name": "Alice"
+    },
+    {
+      "http://example.com/name": "Bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/order.json b/query/linkedql/steps/test-cases/order.json
new file mode 100644
index 0000000..0afe648
--- /dev/null
+++ b/query/linkedql/steps/test-cases/order.json
@@ -0,0 +1,33 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Order",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    }
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    },
+    {
+      "@id": "http://example.com/bob"
+    },
+    {
+      "@id": "http://example.com/likes"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/properties.json b/query/linkedql/steps/test-cases/properties.json
new file mode 100644
index 0000000..b845f4c
--- /dev/null
+++ b/query/linkedql/steps/test-cases/properties.json
@@ -0,0 +1,34 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:Properties",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      },
+      "linkedql:names": ["http://example.com/likes"]
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "http://example.com/likes": {
+        "@id": "http://example.com/bob"
+      }
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/property-names-as.json b/query/linkedql/steps/test-cases/property-names-as.json
new file mode 100644
index 0000000..5b6b69e
--- /dev/null
+++ b/query/linkedql/steps/test-cases/property-names-as.json
@@ -0,0 +1,34 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:PropertyNamesAs",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      },
+      "linkedql:tag": "property"
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "property": {
+        "@id": "http://example.com/likes"
+      }
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/property-names.json b/query/linkedql/steps/test-cases/property-names.json
new file mode 100644
index 0000000..063a732
--- /dev/null
+++ b/query/linkedql/steps/test-cases/property-names.json
@@ -0,0 +1,27 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:PropertyNames",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    }
+  },
+  "results": [
+    {
+      "@id": "http://example.com/likes"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/reg-exp.json b/query/linkedql/steps/test-cases/reg-exp.json
new file mode 100644
index 0000000..7abdcd5
--- /dev/null
+++ b/query/linkedql/steps/test-cases/reg-exp.json
@@ -0,0 +1,21 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/name": ["Alice"]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:RegExp",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:includeIRIs,omitempty": false,
+    "linkedql:expression": "A"
+  },
+  "results": ["Alice"]
+}
diff --git a/query/linkedql/steps/test-cases/reverse-properties.json b/query/linkedql/steps/test-cases/reverse-properties.json
new file mode 100644
index 0000000..15cea3c
--- /dev/null
+++ b/query/linkedql/steps/test-cases/reverse-properties.json
@@ -0,0 +1,34 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:ReverseProperties",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      },
+      "linkedql:names": ["http://example.com/likes"]
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "http://example.com/likes": {
+        "@id": "http://example.com/alice"
+      }
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/reverse-property-names-as.json b/query/linkedql/steps/test-cases/reverse-property-names-as.json
new file mode 100644
index 0000000..111c6c4
--- /dev/null
+++ b/query/linkedql/steps/test-cases/reverse-property-names-as.json
@@ -0,0 +1,34 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:ReversePropertyNamesAs",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      },
+      "linkedql:tag": "property"
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "property": {
+        "@id": "http://example.com/likes"
+      }
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/select-first.json b/query/linkedql/steps/test-cases/select-first.json
new file mode 100644
index 0000000..ef8fd2f
--- /dev/null
+++ b/query/linkedql/steps/test-cases/select-first.json
@@ -0,0 +1,38 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:SelectFirst",
+    "linkedql:from": {
+      "@type": "linkedql:Visit",
+      "linkedql:from": {
+        "@type": "linkedql:As",
+        "linkedql:from": {
+          "@type": "linkedql:Vertex",
+          "linkedql:values": []
+        },
+        "linkedql:name": "http://example.com/liker"
+      },
+      "linkedql:properties": "http://example.com/likes"
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "http://example.com/liker": {
+        "@id": "http://example.com/alice"
+      }
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/select-with-tags.json b/query/linkedql/steps/test-cases/select-with-tags.json
new file mode 100644
index 0000000..e134395
--- /dev/null
+++ b/query/linkedql/steps/test-cases/select-with-tags.json
@@ -0,0 +1,42 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:As",
+      "linkedql:from": {
+        "@type": "linkedql:Visit",
+        "linkedql:from": {
+          "@type": "linkedql:As",
+          "linkedql:from": {
+            "@type": "linkedql:Vertex",
+            "linkedql:values": []
+          },
+          "linkedql:name": "http://example.com/liker"
+        },
+        "linkedql:properties": "http://example.com/likes"
+      },
+      "linkedql:name": "liked"
+    },
+    "linkedql:tags": ["http://example.com/liker"]
+  },
+  "results": [
+    {
+      "http://example.com/liker": {
+        "@id": "http://example.com/alice"
+      }
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/select.json b/query/linkedql/steps/test-cases/select.json
new file mode 100644
index 0000000..81cb3f9
--- /dev/null
+++ b/query/linkedql/steps/test-cases/select.json
@@ -0,0 +1,38 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:Visit",
+      "linkedql:from": {
+        "@type": "linkedql:As",
+        "linkedql:from": {
+          "@type": "linkedql:Vertex",
+          "linkedql:values": []
+        },
+        "linkedql:name": "http://example.com/liker"
+      },
+      "linkedql:properties": "http://example.com/likes"
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "http://example.com/liker": {
+        "@id": "http://example.com/alice"
+      }
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/skip.json b/query/linkedql/steps/test-cases/skip.json
new file mode 100644
index 0000000..641089a
--- /dev/null
+++ b/query/linkedql/steps/test-cases/skip.json
@@ -0,0 +1,28 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Skip",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:offset": 2
+  },
+  "results": [
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/union.json b/query/linkedql/steps/test-cases/union.json
new file mode 100644
index 0000000..857abb7
--- /dev/null
+++ b/query/linkedql/steps/test-cases/union.json
@@ -0,0 +1,44 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Union",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": [
+        {
+          "@id": "http://example.com/alice"
+        }
+      ]
+    },
+    "linkedql:steps": [
+      {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": [
+          {
+            "@id": "http://example.com/bob"
+          }
+        ]
+      }
+    ]
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    },
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/unique.json b/query/linkedql/steps/test-cases/unique.json
new file mode 100644
index 0000000..4582397
--- /dev/null
+++ b/query/linkedql/steps/test-cases/unique.json
@@ -0,0 +1,34 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Unique",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": [
+        { "@id": "http://example.com/alice" },
+        { "@id": "http://example.com/alice" },
+        { "@id": "http://example.com/bob" }
+      ]
+    }
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    },
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/view-reverse.json b/query/linkedql/steps/test-cases/view-reverse.json
new file mode 100644
index 0000000..430c9be
--- /dev/null
+++ b/query/linkedql/steps/test-cases/view-reverse.json
@@ -0,0 +1,28 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:VisitReverse",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:properties": "http://example.com/likes"
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/view.json b/query/linkedql/steps/test-cases/view.json
new file mode 100644
index 0000000..6177056
--- /dev/null
+++ b/query/linkedql/steps/test-cases/view.json
@@ -0,0 +1,28 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Visit",
+    "linkedql:from": {
+      "@type": "linkedql:Vertex",
+      "linkedql:values": []
+    },
+    "linkedql:properties": "http://example.com/likes"
+  },
+  "results": [
+    {
+      "@id": "http://example.com/bob"
+    }
+  ]
+}
diff --git a/query/linkedql/steps/test-cases/where.json b/query/linkedql/steps/test-cases/where.json
new file mode 100644
index 0000000..117c7fd
--- /dev/null
+++ b/query/linkedql/steps/test-cases/where.json
@@ -0,0 +1,65 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ],
+      "http://example.com/name": ["Alice"]
+    },
+    {
+      "@id": "http://example.com/bob",
+      "http://example.com/name": ["Bob"]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Select",
+    "linkedql:from": {
+      "@type": "linkedql:Where",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      },
+      "linkedql:steps": [
+        {
+          "@type": "linkedql:As",
+          "linkedql:from": {
+            "@type": "linkedql:Visit",
+            "linkedql:from": {
+              "@type": "linkedql:Visit",
+              "linkedql:from": {
+                "@type": "linkedql:Placeholder"
+              },
+              "linkedql:properties": "http://example.com/likes"
+            },
+            "linkedql:properties": "http://example.com/name"
+          },
+          "linkedql:name": "http://example.com/likesName"
+        },
+        {
+          "@type": "linkedql:As",
+          "linkedql:from": {
+            "@type": "linkedql:Visit",
+            "linkedql:from": {
+              "@type": "linkedql:Placeholder"
+            },
+            "linkedql:properties": "http://example.com/name"
+          },
+          "linkedql:name": "http://example.com/name"
+        }
+      ]
+    },
+    "linkedql:tags": []
+  },
+  "results": [
+    {
+      "http://example.com/likesName": "Bob",
+      "http://example.com/name": "Alice"
+    }
+  ]
+}