Repository: cayleygraph/cayley
Pull request #925 (2020-03-18 23:08:51)

Namespace Rules HTTP API
Create a new HTTP resource for namespace rules.
The resource supports getting all registered rules and registering a rule.

<!-- 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/925)
<!-- Reviewable:end -->



Patch:
diff --git a/docs/api/swagger.yml b/docs/api/swagger.yml
index 32a5173..15de816 100644
--- a/docs/api/swagger.yml
+++ b/docs/api/swagger.yml
@@ -381,6 +381,50 @@ paths:
             application/json:
               schema:
                 $ref: "#/components/schemas/Error"
+  /api/v2/namespace-rules:
+    get:
+      tags:
+        - "data"
+      summary: "Reads all namespace rules from the database"
+      description: ""
+      operationId: "getNamespaceRules"
+      responses:
+        200:
+          description: "Success"
+          content:
+            "application/json":
+              schema:
+                type: "array"
+                items:
+                  type: "object"
+                  properties:
+                    prefix:
+                      description: "Prefix of the namespace"
+                      type: "string"
+                    namespace:
+                      description: "The namespace prefixed"
+                      type: "string"
+        default:
+          description: "Unexpected error"
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
+    post:
+      tags:
+        - "data"
+      summary: "Registers new namespace rule to the database"
+      description: ""
+      operationId: "registerNamespaceRule"
+      responses:
+        201:
+          description: "Success"
+        default:
+          description: "Unexpected error"
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
   /gephi/gs:
     get:
       tags:
diff --git a/docs/docker-compose/docker-compose.mongo.yml b/docs/docker-compose/docker-compose.mongo.yml
new file mode 100644
index 0000000..e0efbaf
--- /dev/null
+++ b/docs/docker-compose/docker-compose.mongo.yml
@@ -0,0 +1,11 @@
+version: "2.2"
+services:
+  cayley:
+    image: cayleygraph/cayley
+    command: http --db mongo --dbpath mongodb://mongo:27017
+    ports:
+      - 64210:64210
+  mongo:
+    image: mongo
+    ports:
+      - 27017:27017
diff --git a/go.mod b/go.mod
index 58706e3..734df3b 100644
--- a/go.mod
+++ b/go.mod
@@ -4,14 +4,14 @@ go 1.12
 
 require (
 	github.com/badgerodon/peg v0.0.0-20130729175151-9e5f7f4d07ca
-	github.com/cayleygraph/quad v1.2.1
+	github.com/cayleygraph/quad v1.2.4
 	github.com/cockroachdb/apd v1.1.0 // indirect
 	github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc // indirect
 	github.com/coreos/bbolt v1.3.3 // indirect
 	github.com/cznic/mathutil v0.0.0-20170313102836-1447ad269d64
 	github.com/d4l3k/messagediff v1.2.1 // indirect
 	github.com/dennwc/graphql v0.0.0-20180603144102-12cfed44bc5d
-	github.com/dgraph-io/badger v1.5.5
+	github.com/dgraph-io/badger v1.5.5 // indirect
 	github.com/dlclark/regexp2 v1.1.4 // indirect
 	github.com/docker/docker v0.7.3-0.20180412203414-a422774e593b // indirect
 	github.com/docker/go-units v0.4.0 // indirect
@@ -36,9 +36,9 @@ require (
 	github.com/opencontainers/selinux v1.0.0 // indirect
 	github.com/pelletier/go-toml v1.4.0 // indirect
 	github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b
+	github.com/piprate/json-gold v0.3.0
 	github.com/prometheus/client_golang v0.9.3
 	github.com/rogpeppe/go-internal v1.5.0 // indirect
-	github.com/russross/blackfriday v1.5.2
 	github.com/satori/go.uuid v1.2.0 // indirect
 	github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
 	github.com/spf13/afero v1.2.2 // indirect
@@ -50,7 +50,7 @@ require (
 	github.com/syndtr/goleveldb v1.0.0
 	github.com/tylertreat/BoomFilters v0.0.0-20181028192813-611b3dbe80e8
 	go.etcd.io/bbolt v1.3.3 // indirect
-	golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc
+	golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc // indirect
 	golang.org/x/net v0.0.0-20190628185345-da137c7871d7
 	golang.org/x/sys v0.0.0-20191009170203-06d7bd2c5f4f // indirect
 	golang.org/x/tools v0.0.0-20191010075000-0337d82405ff // indirect
diff --git a/go.sum b/go.sum
index 60c818b..4614a9a 100644
--- a/go.sum
+++ b/go.sum
@@ -25,11 +25,10 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
 github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
-github.com/cayleygraph/quad v1.1.0 h1:w1nXAmn+nz07+qlw89dke9LwWkYpeX+OcvfTvGQRBpM=
-github.com/cayleygraph/quad v1.1.0/go.mod h1:maWODEekEhrO0mdc9h5n/oP7cH1h/OTgqQ2qWbuI9M4=
-github.com/cayleygraph/quad v1.2.0/go.mod h1:maWODEekEhrO0mdc9h5n/oP7cH1h/OTgqQ2qWbuI9M4=
-github.com/cayleygraph/quad v1.2.1 h1:PqqP+F8BLICaAWoGvSlrx1U11pXEc2BBodmTv30uSLM=
-github.com/cayleygraph/quad v1.2.1/go.mod h1:maWODEekEhrO0mdc9h5n/oP7cH1h/OTgqQ2qWbuI9M4=
+github.com/cayleygraph/quad v1.2.3 h1:2HXwZGElLBuFLz8/6FdfWtil2W2gdfvYxNzS337SOwo=
+github.com/cayleygraph/quad v1.2.3/go.mod h1:XOianlRdDK5Upno/6svE6APe/wD8XgYrL9smqK875nU=
+github.com/cayleygraph/quad v1.2.4 h1:+6u09WxA7zg9ILonK8DChwzWKLKsDkjyvX+CXXhI/mM=
+github.com/cayleygraph/quad v1.2.4/go.mod h1:XOianlRdDK5Upno/6svE6APe/wD8XgYrL9smqK875nU=
 github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY=
 github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@@ -202,7 +201,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
 github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/linkeddata/gojsonld v0.0.0-20170418210642-4f5db6791326 h1:YP3lfXXYiQV5MKeUqVnxRP5uuMQTLPx+PGYm1UBoU98=
 github.com/linkeddata/gojsonld v0.0.0-20170418210642-4f5db6791326/go.mod h1:nfqkuSNlsk1bvti/oa7TThx4KmRMBmSxf3okHI9wp3E=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
@@ -246,11 +244,15 @@ github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUr
 github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b h1:8uaXtUkxiy+T/zdLWuxa/PG4so0TPZDZfafFNNSaptE=
 github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/piprate/json-gold v0.3.0 h1:a1vHx7Q1jOO1pjCtKwTI/WCzwaQwRt9VM7apK2uy200=
+github.com/piprate/json-gold v0.3.0/go.mod h1:OK1z7UgtBZk06n2cDE2OSq1kffmjFFp5/2yhLLCz9UM=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
+github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
 github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
diff --git a/internal/linkedql/schema/schema.go b/internal/linkedql/schema/schema.go
index 575f6c9..c3b0dd0 100644
--- a/internal/linkedql/schema/schema.go
+++ b/internal/linkedql/schema/schema.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/cayleygraph/cayley/query/linkedql"
+	// Steps are imported here so they be registered and documented in the schema
 	_ "github.com/cayleygraph/cayley/query/linkedql/steps"
 	"github.com/cayleygraph/quad"
 	"github.com/cayleygraph/quad/voc/owl"
@@ -26,8 +27,7 @@ var (
 	iteratorStep     = reflect.TypeOf((*linkedql.IteratorStep)(nil)).Elem()
 	entityIdentifier = reflect.TypeOf((*linkedql.EntityIdentifier)(nil)).Elem()
 	value            = reflect.TypeOf((*quad.Value)(nil)).Elem()
-	operator         = reflect.TypeOf((*linkedql.Operator)(nil)).Elem()
-	propertyPath     = reflect.TypeOf((*linkedql.PropertyPath)(nil)).Elem()
+	propertyPath     = reflect.TypeOf((*linkedql.PropertyPath)(nil))
 	stringMap        = reflect.TypeOf(map[string]string{})
 	graphPattern     = reflect.TypeOf(linkedql.GraphPattern(nil))
 )
@@ -51,18 +51,15 @@ func typeToRange(t reflect.Type) string {
 	if kind := t.Kind(); kind == reflect.Int64 || kind == reflect.Int {
 		return xsd.Int
 	}
-	if t.Implements(pathStep) {
-		return linkedql.Prefix + "PathStep"
-	}
-	if t.Implements(operator) {
-		return linkedql.Prefix + "Operator"
-	}
 	if t.Implements(value) {
 		return rdfs.Resource
 	}
-	if t.Implements(entityIdentifier) {
+	if t == entityIdentifier {
 		return owl.Thing
 	}
+	if t == pathStep {
+		return linkedql.Prefix + "PathStep"
+	}
 	if t == propertyPath {
 		return linkedql.Prefix + "PropertyPath"
 	}
diff --git a/query/linkedql/entity_identfier.go b/query/linkedql/entity_identfier.go
index 72e674f..8f8e2cf 100644
--- a/query/linkedql/entity_identfier.go
+++ b/query/linkedql/entity_identfier.go
@@ -1,14 +1,58 @@
 package linkedql
 
-import "github.com/cayleygraph/quad"
+import (
+	"encoding/json"
 
-import "github.com/cayleygraph/quad/voc"
+	"github.com/cayleygraph/quad"
+	"github.com/cayleygraph/quad/voc"
+)
 
-// EntityIdentifier is an interface to be used where a single entity identifier is expected.
-type EntityIdentifier interface {
+// EntityIdentifierI is an interface to be used where a single entity identifier is expected.
+type EntityIdentifierI interface {
 	BuildIdentifier(ns *voc.Namespaces) (quad.Value, error)
 }
 
+// EntityIdentifier is a struct wrapping the interface EntityIdentifierI
+type EntityIdentifier struct {
+	EntityIdentifierI
+}
+
+// NewEntityIdentifier constructs a new EntityIdentifer from a EntityIdentiferI
+func NewEntityIdentifier(v EntityIdentifierI) EntityIdentifier {
+	return EntityIdentifier{EntityIdentifierI: v}
+}
+
+// UnmarshalJSON implements RawMessage
+func (p *EntityIdentifier) UnmarshalJSON(data []byte) error {
+	var errors []error
+
+	var iri EntityIRI
+	err := json.Unmarshal(data, &iri)
+	if err == nil {
+		p.EntityIdentifierI = iri
+		return nil
+	}
+	errors = append(errors, err)
+
+	var bnode EntityBNode
+	err = json.Unmarshal(data, &bnode)
+	if err == nil {
+		p.EntityIdentifierI = bnode
+		return nil
+	}
+	errors = append(errors, err)
+
+	var s EntityIdentifierString
+	err = json.Unmarshal(data, &s)
+	if err == nil {
+		p.EntityIdentifierI = s
+		return nil
+	}
+	errors = append(errors, err)
+
+	return formatMultiError(errors)
+}
+
 // EntityIRI is an entity IRI.
 type EntityIRI quad.IRI
 
diff --git a/query/linkedql/iter_docs.go b/query/linkedql/iter_docs.go
index 335f015..a7795b6 100644
--- a/query/linkedql/iter_docs.go
+++ b/query/linkedql/iter_docs.go
@@ -4,49 +4,48 @@ import (
 	"context"
 
 	"github.com/cayleygraph/cayley/query"
-	"github.com/cayleygraph/quad"
+	"github.com/piprate/json-gold/ld"
 )
 
-var _ query.Iterator = (*DocumentIterator)(nil)
-
-type document = map[string]interface{}
-type properties = map[string][]interface{}
-type idToProperties = map[quad.Value]properties
+var (
+	_ query.Iterator = (*DocumentIterator)(nil)
+)
 
 // DocumentIterator is an iterator of documents from the graph
 type DocumentIterator struct {
-	tagsIt     *TagsIterator
-	ids        []quad.Value
-	properties idToProperties
-	current    int
+	tagsIt    *TagsIterator
+	dataset   *ld.RDFDataset
+	err       error
+	exhausted bool
 }
 
 // NewDocumentIterator returns a new DocumentIterator for a QuadStore and Path.
 func NewDocumentIterator(valueIt *ValueIterator) *DocumentIterator {
 	tagsIt := &TagsIterator{ValueIt: valueIt, Selected: nil}
-	return &DocumentIterator{tagsIt: tagsIt, current: -1}
+	return &DocumentIterator{tagsIt: tagsIt, exhausted: false}
+}
+
+func (it *DocumentIterator) getDataset(ctx context.Context) (*ld.RDFDataset, error) {
+	d := ld.NewRDFDataset()
+	for it.tagsIt.Next(ctx) {
+		err := it.tagsIt.addResultsToDataset(d)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return d, nil
 }
 
 // Next implements query.Iterator.
 func (it *DocumentIterator) Next(ctx context.Context) bool {
-	if it.properties == nil {
-		it.properties = make(idToProperties)
-		for it.tagsIt.Next(ctx) {
-			id := it.tagsIt.ValueIt.Value()
-			tags := it.tagsIt.getTags()
-			it.ids = append(it.ids, id)
-			for k, v := range tags {
-				m, ok := it.properties[id]
-				if !ok {
-					m = make(properties)
-					it.properties[id] = m
-				}
-				m[k] = append(m[k], v)
-			}
+	if !it.exhausted {
+		d, err := it.getDataset(ctx)
+		if err != nil {
+			it.err = err
+		} else {
+			it.dataset = d
 		}
-	}
-	if it.current < len(it.ids)-1 {
-		it.current++
+		it.exhausted = true
 		return true
 	}
 	return false
@@ -54,25 +53,13 @@ func (it *DocumentIterator) Next(ctx context.Context) bool {
 
 // Result implements query.Iterator.
 func (it *DocumentIterator) Result() interface{} {
-	if it.current >= len(it.ids) {
-		return nil
-	}
-	id := it.ids[it.current]
-	// FIXME(iddan): don't cast to string when collation is Raw
-	var sid string
-	switch val := id.(type) {
-	case quad.IRI:
-		sid = string(val)
-	case quad.BNode:
-		sid = val.String()
-	}
-	d := document{
-		"@id": sid,
-	}
-	for k, v := range it.properties[id] {
-		d[k] = v
+	context := make(map[string]interface{})
+	opts := ld.NewJsonLdOptions("")
+	c, err := datasetToCompact(it.dataset, context, opts)
+	if err != nil {
+		it.err = err
 	}
-	return d
+	return c
 }
 
 // Err implements query.Iterator.
diff --git a/query/linkedql/iter_tags.go b/query/linkedql/iter_tags.go
index c0b0c76..6eef2d7 100644
--- a/query/linkedql/iter_tags.go
+++ b/query/linkedql/iter_tags.go
@@ -2,10 +2,13 @@ package linkedql
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/cayleygraph/cayley/graph/refs"
 	"github.com/cayleygraph/cayley/query"
+	"github.com/cayleygraph/quad"
 	"github.com/cayleygraph/quad/jsonld"
+	"github.com/piprate/json-gold/ld"
 )
 
 var _ query.Iterator = (*TagsIterator)(nil)
@@ -13,8 +16,20 @@ var _ query.Iterator = (*TagsIterator)(nil)
 // TagsIterator is a result iterator for records consisting of selected tags
 // or all the tags in the query.
 type TagsIterator struct {
-	ValueIt  *ValueIterator
-	Selected []string
+	ValueIt   *ValueIterator
+	Selected  []string
+	ExcludeID bool
+	err       error
+}
+
+// NewTagsIterator creates a new TagsIterator
+func NewTagsIterator(valueIt *ValueIterator, selected []string, excludeID bool) TagsIterator {
+	return TagsIterator{
+		ValueIt:   valueIt,
+		Selected:  selected,
+		ExcludeID: excludeID,
+		err:       nil,
+	}
 }
 
 // Next implements query.Iterator.
@@ -22,32 +37,76 @@ func (it *TagsIterator) Next(ctx context.Context) bool {
 	return it.ValueIt.Next(ctx)
 }
 
-func (it *TagsIterator) getTags() map[string]interface{} {
+func (it *TagsIterator) addQuadFromRef(dataset *ld.RDFDataset, subject ld.Node, tag string, ref refs.Ref) error {
+	p := ld.NewIRI(tag)
+	o, err := jsonld.ToNode(it.ValueIt.getName(ref))
+	if err != nil {
+		return err
+	}
+	q := ld.NewQuad(subject, p, o, "")
+	dataset.Graphs["@default"] = append(dataset.Graphs["@default"], q)
+	return nil
+}
+
+func getSubject(it *TagsIterator) (ld.Node, error) {
+	r := it.ValueIt.scanner.Result()
+	identifier, ok := it.ValueIt.getName(r).(quad.Identifier)
+	if !ok {
+		return nil, fmt.Errorf("Expected subject to be an entity identifier but instead received: %v", identifier)
+	}
+	return jsonld.ToNode(identifier)
+}
+
+func (it *TagsIterator) addResultsToDataset(dataset *ld.RDFDataset) error {
+	s, err := getSubject(it)
+	if err != nil {
+		return err
+	}
+
 	refTags := make(map[string]refs.Ref)
+
 	it.ValueIt.scanner.TagResults(refTags)
 
-	tags := make(map[string]interface{})
-	// FIXME(iddan): only convert when collation is JSON/JSON-LD, leave as Ref otherwise
-	if it.Selected != nil {
-		for _, tag := range it.Selected {
-			tags[tag] = jsonld.FromValue(it.ValueIt.getName(refTags[tag]))
+	if len(it.Selected) == 0 {
+		for tag, ref := range refTags {
+			it.addQuadFromRef(dataset, s, tag, ref)
 		}
 	} else {
-		for tag, ref := range refTags {
-			tags[tag] = jsonld.FromValue(it.ValueIt.getName(ref))
+		for _, tag := range it.Selected {
+			it.addQuadFromRef(dataset, s, tag, refTags[tag])
 		}
 	}
 
-	return tags
+	return nil
 }
 
 // Result implements query.Iterator.
 func (it *TagsIterator) Result() interface{} {
-	return it.getTags()
+	// FIXME(iddan): only convert when collation is JSON/JSON-LD, leave as Ref otherwise
+	d := ld.NewRDFDataset()
+	err := it.addResultsToDataset(d)
+	if err != nil {
+		it.err = err
+		return nil
+	}
+	r, err := singleDocumentFromRDF(d)
+	if err != nil {
+		it.err = err
+		return nil
+	}
+	if !it.ExcludeID {
+		m := r.(map[string]interface{})
+		delete(m, "@id")
+		return m
+	}
+	return r
 }
 
 // Err implements query.Iterator.
 func (it *TagsIterator) Err() error {
+	if it.err != nil {
+		return it.err
+	}
 	return it.ValueIt.Err()
 }
 
diff --git a/query/linkedql/jsonld_util.go b/query/linkedql/jsonld_util.go
new file mode 100644
index 0000000..dd5f2de
--- /dev/null
+++ b/query/linkedql/jsonld_util.go
@@ -0,0 +1,36 @@
+package linkedql
+
+import (
+	"fmt"
+
+	"github.com/piprate/json-gold/ld"
+)
+
+// datasetToCompact transforms a RDF dataset to a compact JSON-LD document.
+func datasetToCompact(dataset *ld.RDFDataset, context interface{}, opts *ld.JsonLdOptions) (interface{}, error) {
+	api := ld.NewJsonLdApi()
+	proc := ld.NewJsonLdProcessor()
+	d, err := api.FromRDF(dataset, opts)
+	if err != nil {
+		return nil, err
+	}
+	c, err := proc.Compact(d, context, opts)
+	if err != nil {
+		return nil, err
+	}
+	return c, nil
+}
+
+// singleDocumentFromRDF transforms a RDF dataset to a single map JSON-LD document.
+func singleDocumentFromRDF(dataset *ld.RDFDataset) (interface{}, error) {
+	api := ld.NewJsonLdApi()
+	opts := ld.NewJsonLdOptions("")
+	documents, err := api.FromRDF(dataset, opts)
+	if err != nil {
+		return nil, err
+	}
+	if len(documents) != 1 {
+		return nil, fmt.Errorf("Unexpected number of documents")
+	}
+	return documents[0], nil
+}
diff --git a/query/linkedql/operator.go b/query/linkedql/operator.go
deleted file mode 100644
index 99ef3ad..0000000
--- a/query/linkedql/operator.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package linkedql
-
-import "github.com/cayleygraph/cayley/query/path"
-
-// Operator represents an operator used in a query inside a step (e.g. greater than).
-type Operator interface {
-	RegistryItem
-	Apply(p *path.Path) (*path.Path, error)
-}
diff --git a/query/linkedql/property_path.go b/query/linkedql/property_path.go
index d823757..c66ac0c 100644
--- a/query/linkedql/property_path.go
+++ b/query/linkedql/property_path.go
@@ -2,6 +2,7 @@ package linkedql
 
 import (
 	"encoding/json"
+	"fmt"
 
 	"github.com/cayleygraph/cayley/graph"
 	"github.com/cayleygraph/cayley/query/path"
@@ -9,15 +10,21 @@ import (
 	"github.com/cayleygraph/quad/voc"
 )
 
+// PropertyPathI is an interface to be used where a path of properties is expected.
 type PropertyPathI interface {
 	BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error)
 }
 
-// PropertyPath is an interface to be used where a path of properties is expected.
+// PropertyPath is a struct wrapping PropertyPathI
 type PropertyPath struct {
 	PropertyPathI
 }
 
+// NewPropertyPath constructs a new PropertyPath
+func NewPropertyPath(p PropertyPathI) *PropertyPath {
+	return &PropertyPath{PropertyPathI: p}
+}
+
 // Description implements Step.
 func (*PropertyPath) Description() string {
 	return "PropertyPath is a string, multiple strins or path describing a set of properties"
@@ -59,6 +66,17 @@ func (p *PropertyPath) UnmarshalJSON(data []byte) error {
 	}
 	errors = append(errors, err)
 
+	step, err := Unmarshal(data)
+	if err == nil {
+		pathStep, ok := step.(PathStep)
+		if ok {
+			p.PropertyPathI = pathStep
+			return nil
+		}
+		errors = append(errors, fmt.Errorf("Step of type %T is not a PathStep. A PropertyPath step must be a PathStep", step))
+	}
+	errors = append(errors, err)
+
 	return formatMultiError(errors)
 }
 
@@ -77,13 +95,18 @@ func (p PropertyIRIs) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.P
 // PropertyIRIStrings is a slice of property IRI strings.
 type PropertyIRIStrings []string
 
-// BuildPath implements PropertyPath.
-func (p PropertyIRIStrings) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
+// PropertyIRIs casts PropertyIRIStrings into PropertyIRIs
+func (p PropertyIRIStrings) PropertyIRIs() PropertyIRIs {
 	var iris PropertyIRIs
 	for _, iri := range p {
 		iris = append(iris, PropertyIRI(iri))
 	}
-	return iris.BuildPath(qs, ns)
+	return iris
+}
+
+// BuildPath implements PropertyPath.
+func (p PropertyIRIStrings) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
+	return p.PropertyIRIs().BuildPath(qs, ns)
 }
 
 // PropertyIRI is an IRI of a Property
@@ -106,3 +129,13 @@ func (p PropertyIRIString) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*p
 	iri := PropertyIRI(p)
 	return iri.BuildPath(qs, ns)
 }
+
+// PropertyStep is a step that should resolve to a path of properties
+type PropertyStep struct {
+	PathStep
+}
+
+// BuildPath implements PropertyPath
+func (p PropertyStep) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
+	return p.BuildPath(qs, ns)
+}
diff --git a/query/linkedql/registry.go b/query/linkedql/registry.go
index ebcc11d..eca706f 100644
--- a/query/linkedql/registry.go
+++ b/query/linkedql/registry.go
@@ -105,7 +105,7 @@ func Unmarshal(data []byte) (RegistryItem, error) {
 			if err != nil {
 				return nil, err
 			}
-			value, err := parseValue(v)
+			value, err := parseValue(a)
 			if err != nil {
 				return nil, err
 			}
@@ -247,6 +247,10 @@ func parseLiteral(a interface{}) (quad.Value, error) {
 	case int64:
 		return quad.Int(a), nil
 	case float64:
+		i := int64(a)
+		if a == float64(i) {
+			return quad.Int(i), nil
+		}
 		return quad.Float(a), nil
 	case bool:
 		return quad.Bool(a), nil
diff --git a/query/linkedql/steps/both.go b/query/linkedql/steps/both.go
index 2d0d33c..eb75d7d 100644
--- a/query/linkedql/steps/both.go
+++ b/query/linkedql/steps/both.go
@@ -17,8 +17,8 @@ var _ linkedql.PathStep = (*Both)(nil)
 
 // Both corresponds to .both().
 type Both struct {
-	From       linkedql.PathStep     `json:"from"`
-	Properties linkedql.PropertyPath `json:"properties"`
+	From       linkedql.PathStep      `json:"from"`
+	Properties *linkedql.PropertyPath `json:"properties"`
 }
 
 // Description implements Step.
diff --git a/query/linkedql/steps/collect.go b/query/linkedql/steps/collect.go
new file mode 100644
index 0000000..5ce5f62
--- /dev/null
+++ b/query/linkedql/steps/collect.go
@@ -0,0 +1,58 @@
+package steps
+
+import (
+	"github.com/cayleygraph/cayley/graph"
+	"github.com/cayleygraph/cayley/query"
+	"github.com/cayleygraph/cayley/query/linkedql"
+	"github.com/cayleygraph/cayley/query/path"
+	"github.com/cayleygraph/quad"
+	"github.com/cayleygraph/quad/voc"
+)
+
+func init() {
+	linkedql.Register(&Collect{})
+}
+
+var _ linkedql.IteratorStep = (*Collect)(nil)
+var _ linkedql.PathStep = (*Collect)(nil)
+
+// Collect corresponds to .view().
+type Collect struct {
+	From linkedql.PathStep `json:"from"`
+	Name quad.IRI          `json:"name"`
+}
+
+// Description implements Step.
+func (s *Collect) Description() string {
+	return "Recursively resolves values of a list (also known as RDF collection)"
+}
+
+// BuildIterator implements linkedql.IteratorStep.
+func (s *Collect) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (query.Iterator, error) {
+	return linkedql.NewValueIteratorFromPathStep(s, qs, ns)
+}
+
+var (
+	first  = quad.IRI("rdf:first").Full()
+	rest   = quad.IRI("rdf:rest").Full()
+	rdfNil = quad.IRI("rdf:nil").Full()
+)
+
+// BuildPath implements linkedql.PathStep.
+func (s *Collect) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
+	fromPath, err := s.From.BuildPath(qs, ns)
+	if err != nil {
+		return nil, err
+	}
+	p := fromPath.
+		Out(s.Name).
+		Save(first, string(first)).
+		Save(rest, string(rest)).
+		Or(
+			fromPath.Out(s.Name).FollowRecursive(rest, -1, nil).
+				Save(first, string(first)).
+				Save(rest, string(rest)),
+		).
+		Or(fromPath.Save(s.Name, string(s.Name)))
+	return p, nil
+}
diff --git a/query/linkedql/steps/entities.go b/query/linkedql/steps/entities.go
deleted file mode 100644
index f5d0e4f..0000000
--- a/query/linkedql/steps/entities.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package steps
-
-import (
-	"github.com/cayleygraph/cayley/graph"
-	"github.com/cayleygraph/cayley/query"
-	"github.com/cayleygraph/cayley/query/linkedql"
-	"github.com/cayleygraph/cayley/query/path"
-	"github.com/cayleygraph/quad"
-	"github.com/cayleygraph/quad/voc"
-)
-
-func init() {
-	linkedql.Register(&Entities{})
-}
-
-var _ linkedql.IteratorStep = (*Entities)(nil)
-var _ linkedql.PathStep = (*Entities)(nil)
-
-// Entities corresponds to g.Entities().
-type Entities struct {
-	Identifiers []linkedql.EntityIdentifier `json:"identifiers"`
-}
-
-// Description implements Step.
-func (s *Entities) Description() string {
-	return "resolves to all the existing objects in the graph. If provided with identifiers resolves to a sublist of all the existing identifiers in the graph."
-}
-
-// BuildIterator implements linkedql.IteratorStep.
-func (s *Entities) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (query.Iterator, error) {
-	return linkedql.NewValueIteratorFromPathStep(s, qs, ns)
-}
-
-// BuildPath implements linkedql.PathStep.
-func (s *Entities) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
-	var values []quad.Value
-	for _, identifier := range s.Identifiers {
-		value, err := identifier.BuildIdentifier(ns)
-		if err != nil {
-			return nil, err
-		}
-		values = append(values, value)
-	}
-	// TODO(iddan): Construct a path that only match entities
-	return path.StartPath(qs, values...), nil
-}
diff --git a/query/linkedql/steps/entity.go b/query/linkedql/steps/entity.go
deleted file mode 100644
index dd414eb..0000000
--- a/query/linkedql/steps/entity.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package steps
-
-import (
-	"github.com/cayleygraph/cayley/graph"
-	"github.com/cayleygraph/cayley/query"
-	"github.com/cayleygraph/cayley/query/linkedql"
-	"github.com/cayleygraph/cayley/query/path"
-	"github.com/cayleygraph/quad/voc"
-)
-
-func init() {
-	linkedql.Register(&Entity{})
-}
-
-var _ linkedql.IteratorStep = (*Entity)(nil)
-var _ linkedql.PathStep = (*Entity)(nil)
-
-// Entity corresponds to g.Entity().
-type Entity struct {
-	Identifier linkedql.EntityIdentifier `json:"identifier"`
-}
-
-// Description implements Step.
-func (s *Entity) Description() string {
-	return "resolves to the object matching given identifier in the graph."
-}
-
-// BuildIterator implements linkedql.IteratorStep.
-func (s *Entity) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (query.Iterator, error) {
-	return linkedql.NewValueIteratorFromPathStep(s, qs, ns)
-}
-
-// BuildPath implements linkedql.PathStep.
-func (s *Entity) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
-	identifier, err := s.Identifier.BuildIdentifier(ns)
-	if err != nil {
-		return nil, err
-	}
-	return path.StartPath(qs, identifier), nil
-}
diff --git a/query/linkedql/steps/filter.go b/query/linkedql/steps/filter.go
deleted file mode 100644
index 985094f..0000000
--- a/query/linkedql/steps/filter.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package steps
-
-import (
-	"github.com/cayleygraph/cayley/graph"
-	"github.com/cayleygraph/cayley/query"
-	"github.com/cayleygraph/cayley/query/linkedql"
-	"github.com/cayleygraph/cayley/query/path"
-	"github.com/cayleygraph/quad/voc"
-)
-
-func init() {
-	linkedql.Register(&Filter{})
-}
-
-var _ linkedql.IteratorStep = (*Filter)(nil)
-var _ linkedql.PathStep = (*Filter)(nil)
-
-// Filter corresponds to filter().
-type Filter struct {
-	From   linkedql.PathStep `json:"from"`
-	Filter linkedql.Operator `json:"filter"`
-}
-
-// Description implements Step.
-func (s *Filter) Description() string {
-	return "applies constraints to a set of nodes. Can be used to filter values by range or match strings."
-}
-
-// BuildIterator implements linkedql.IteratorStep.
-func (s *Filter) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (query.Iterator, error) {
-	return linkedql.NewValueIteratorFromPathStep(s, qs, ns)
-}
-
-// BuildPath implements linkedql.PathStep.
-func (s *Filter) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
-	fromIt, err := s.From.BuildPath(qs, ns)
-	if err != nil {
-		return nil, err
-	}
-	return s.Filter.Apply(fromIt)
-}
diff --git a/query/linkedql/steps/follow_reverse.go b/query/linkedql/steps/follow_reverse.go
deleted file mode 100644
index 6e601c0..0000000
--- a/query/linkedql/steps/follow_reverse.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package steps
-
-import (
-	"github.com/cayleygraph/cayley/graph"
-	"github.com/cayleygraph/cayley/query"
-	"github.com/cayleygraph/cayley/query/linkedql"
-	"github.com/cayleygraph/cayley/query/path"
-	"github.com/cayleygraph/quad/voc"
-)
-
-func init() {
-	linkedql.Register(&FollowReverse{})
-}
-
-var _ linkedql.IteratorStep = (*FollowReverse)(nil)
-var _ linkedql.PathStep = (*FollowReverse)(nil)
-
-// FollowReverse corresponds to .followR().
-type FollowReverse struct {
-	From     linkedql.PathStep `json:"from"`
-	Followed linkedql.PathStep `json:"followed"`
-}
-
-// Description implements Step.
-func (s *FollowReverse) Description() string {
-	return "is the same as follow but follows the chain in the reverse direction. Flips View and ViewReverse where appropriate, the net result being a virtual predicate followed in the reverse direction. Starts at the end of the morphism and follows it backwards (with appropriate flipped directions) to the g.M() location."
-}
-
-// BuildIterator implements linkedql.IteratorStep.
-func (s *FollowReverse) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (query.Iterator, error) {
-	return linkedql.NewValueIteratorFromPathStep(s, qs, ns)
-}
-
-// BuildPath implements linkedql.PathStep.
-func (s *FollowReverse) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
-	fromPath, err := s.From.BuildPath(qs, ns)
-	if err != nil {
-		return nil, err
-	}
-	p, err := s.Followed.BuildPath(qs, ns)
-	if err != nil {
-		return nil, err
-	}
-	return fromPath.FollowReverse(p), nil
-}
diff --git a/query/linkedql/steps/has.go b/query/linkedql/steps/has.go
index 683889c..459bac0 100644
--- a/query/linkedql/steps/has.go
+++ b/query/linkedql/steps/has.go
@@ -18,9 +18,9 @@ var _ linkedql.PathStep = (*Has)(nil)
 
 // Has corresponds to .has().
 type Has struct {
-	From     linkedql.PathStep     `json:"from"`
-	Property linkedql.PropertyPath `json:"property"`
-	Values   []quad.Value          `json:"values"`
+	From     linkedql.PathStep      `json:"from"`
+	Property *linkedql.PropertyPath `json:"property"`
+	Values   []quad.Value           `json:"values"`
 }
 
 // Description implements Step.
diff --git a/query/linkedql/steps/has_reverse.go b/query/linkedql/steps/has_reverse.go
index 5ae1fae..2c6287c 100644
--- a/query/linkedql/steps/has_reverse.go
+++ b/query/linkedql/steps/has_reverse.go
@@ -18,9 +18,9 @@ var _ linkedql.PathStep = (*HasReverse)(nil)
 
 // HasReverse corresponds to .hasR().
 type HasReverse struct {
-	From     linkedql.PathStep     `json:"from"`
-	Property linkedql.PropertyPath `json:"property"`
-	Values   []quad.Value          `json:"values"`
+	From     linkedql.PathStep      `json:"from"`
+	Property *linkedql.PropertyPath `json:"property"`
+	Values   []quad.Value           `json:"values"`
 }
 
 // Description implements Step.
diff --git a/query/linkedql/steps/is.go b/query/linkedql/steps/is.go
deleted file mode 100644
index 31ab8f2..0000000
--- a/query/linkedql/steps/is.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package steps
-
-import (
-	"github.com/cayleygraph/cayley/graph"
-	"github.com/cayleygraph/cayley/query"
-	"github.com/cayleygraph/cayley/query/linkedql"
-	"github.com/cayleygraph/cayley/query/path"
-	"github.com/cayleygraph/quad"
-	"github.com/cayleygraph/quad/voc"
-)
-
-func init() {
-	linkedql.Register(&Is{})
-}
-
-var _ linkedql.IteratorStep = (*Is)(nil)
-var _ linkedql.PathStep = (*Is)(nil)
-
-// Is corresponds to .back().
-type Is struct {
-	From   linkedql.PathStep `json:"from"`
-	Values []quad.Value      `json:"values"`
-}
-
-// Description implements Step.
-func (s *Is) Description() string {
-	return "resolves to all the values resolved by the from step which are included in provided values."
-}
-
-// BuildIterator implements linkedql.IteratorStep.
-func (s *Is) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (query.Iterator, error) {
-	return linkedql.NewValueIteratorFromPathStep(s, qs, ns)
-}
-
-// BuildPath implements linkedql.PathStep.
-func (s *Is) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
-	fromPath, err := s.From.BuildPath(qs, ns)
-	if err != nil {
-		return nil, err
-	}
-	return fromPath.Is(linkedql.AbsoluteValues(s.Values, ns)...), nil
-}
diff --git a/query/linkedql/steps/jsonld_util.go b/query/linkedql/steps/jsonld_util.go
new file mode 100644
index 0000000..7755a8f
--- /dev/null
+++ b/query/linkedql/steps/jsonld_util.go
@@ -0,0 +1,78 @@
+package steps
+
+import (
+	"fmt"
+)
+
+type ldArray = []interface{}
+type ldMap = map[string]interface{}
+
+func unwrapValue(i interface{}) interface{} {
+	m, ok := i.(ldMap)
+	if ok && len(m) == 1 {
+		v, ok := m["@value"]
+		if ok {
+			return v
+		}
+	}
+	return i
+}
+
+func unwrapSingle(i interface{}) interface{} {
+	a, ok := i.(ldArray)
+	if ok && len(a) == 1 {
+		return a[0]
+	}
+	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(unwrapSingle(source))
+	target = unwrapValue(unwrapSingle(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 multiple values but instead received the single value: %#v", 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
+}
diff --git a/query/linkedql/steps/less_than.go b/query/linkedql/steps/less_than.go
index 3955449..2c4b76e 100644
--- a/query/linkedql/steps/less_than.go
+++ b/query/linkedql/steps/less_than.go
@@ -1,14 +1,13 @@
-
 package steps
 
 import (
 	"github.com/cayleygraph/cayley/graph"
 	"github.com/cayleygraph/cayley/graph/iterator"
 	"github.com/cayleygraph/cayley/query"
+	"github.com/cayleygraph/cayley/query/linkedql"
 	"github.com/cayleygraph/cayley/query/path"
 	"github.com/cayleygraph/quad"
 	"github.com/cayleygraph/quad/voc"
-	"github.com/cayleygraph/cayley/query/linkedql"
 )
 
 func init() {
@@ -20,13 +19,10 @@ var _ linkedql.PathStep = (*LessThan)(nil)
 
 // LessThan corresponds to lt().
 type LessThan struct {
-	From  linkedql.PathStep   `json:"from"`
-	Value quad.Value `json:"value"`
+	From  linkedql.PathStep `json:"from"`
+	Value quad.Value        `json:"value"`
 }
 
-
-
-
 // Description implements Step.
 func (s *LessThan) Description() string {
 	return "Less than filters out values that are not less than given value"
@@ -45,4 +41,3 @@ func (s *LessThan) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path
 	}
 	return fromPath.Filter(iterator.CompareLT, s.Value), nil
 }
-
diff --git a/query/linkedql/steps/like.go b/query/linkedql/steps/like.go
new file mode 100644
index 0000000..9de37aa
--- /dev/null
+++ b/query/linkedql/steps/like.go
@@ -0,0 +1,42 @@
+package steps
+
+import (
+	"github.com/cayleygraph/cayley/graph"
+	"github.com/cayleygraph/cayley/query"
+	"github.com/cayleygraph/cayley/query/linkedql"
+	"github.com/cayleygraph/cayley/query/path"
+	"github.com/cayleygraph/cayley/query/shape"
+	"github.com/cayleygraph/quad/voc"
+)
+
+func init() {
+	linkedql.Register(&Like{})
+}
+
+var _ linkedql.IteratorStep = (*Like)(nil)
+var _ linkedql.PathStep = (*Like)(nil)
+
+// Like corresponds to like().
+type Like struct {
+	From    linkedql.PathStep `json:"from"`
+	Pattern string            `json:"likePattern"`
+}
+
+// Description implements Operator.
+func (s *Like) Description() string {
+	return "Like filters out values that do not match given pattern."
+}
+
+// BuildIterator implements linkedql.IteratorStep.
+func (s *Like) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (query.Iterator, error) {
+	return linkedql.NewValueIteratorFromPathStep(s, qs, ns)
+}
+
+// BuildPath implements PathStep.
+func (s *Like) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
+	fromPath, err := s.From.BuildPath(qs, ns)
+	if err != nil {
+		return nil, err
+	}
+	return fromPath.Filters(shape.Wildcard{Pattern: s.Pattern}), nil
+}
diff --git a/query/linkedql/steps/match.go b/query/linkedql/steps/match.go
index da2bedf..94eff58 100644
--- a/query/linkedql/steps/match.go
+++ b/query/linkedql/steps/match.go
@@ -57,12 +57,43 @@ func (s *Match) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, e
 		return nil, err
 	}
 
-	// Group quads to subtrees
-	entities := make(map[quad.Value]map[quad.Value][]quad.Value)
-	for _, q := range quads {
+	return p.Follow(buildPatternPath(quads, ns)), nil
+}
+
+type entityProperties = map[quad.Value]map[quad.Value][]quad.Value
+
+func entityPropertiesToPath(entity quad.Value, entities entityProperties) *path.Path {
+	p := path.StartMorphism()
+	if iri, ok := entity.(quad.IRI); ok {
+		p = p.Is(iri)
+	}
+	for property, values := range entities[entity] {
+		for _, value := range values {
+			if _, ok := value.(quad.BNode); ok {
+				p = p.Out(property).Follow(entityPropertiesToPath(value, entities)).Back("")
+			} else {
+				p = p.Has(property, value)
+			}
+		}
+	}
+	return p
+}
+
+func groupEntities(pattern []quad.Quad, ns *voc.Namespaces) (entityProperties, map[quad.Value]struct{}) {
+	// referenced holds entities used as values for properties of other entities
+	referenced := make(map[quad.Value]struct{})
+
+	entities := make(entityProperties)
+
+	for _, q := range pattern {
 		entity := linkedql.AbsoluteValue(q.Subject, ns)
 		property := linkedql.AbsoluteValue(q.Predicate, ns)
 		value := linkedql.AbsoluteValue(q.Object, ns)
+
+		if _, ok := value.(quad.BNode); ok {
+			referenced[value] = struct{}{}
+		}
+
 		properties, ok := entities[entity]
 		if !ok {
 			properties = make(map[quad.Value][]quad.Value)
@@ -74,20 +105,25 @@ func (s *Match) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, e
 		properties[property] = append(properties[property], value)
 	}
 
-	for entity, properties := range entities {
-		if iri, ok := entity.(quad.IRI); ok {
-			p = p.Is(iri)
-		}
-		// FIXME(iddan): this currently flattens all nested objects, which is totally incorrect; recurse or limit allowed json-ld
-		for property, values := range properties {
-			p = p.Has(property, values...)
+	return entities, referenced
+}
+
+// buildPath for given pattern constructs a Path object
+func buildPatternPath(pattern []quad.Quad, ns *voc.Namespaces) *path.Path {
+	entities, referenced := groupEntities(pattern, ns)
+	p := path.StartMorphism()
+
+	for entity := range entities {
+		// Apply only on entities which are not referenced as values
+		if _, ok := referenced[entity]; !ok {
+			p = p.Follow(entityPropertiesToPath(entity, entities))
 		}
 	}
 
-	return p, nil
+	return p
 }
 
-func parsePattern(pattern linkedql.GraphPattern, ns *voc.Namespaces) ([]quad.Quad, error) {
+func contextualizePattern(pattern linkedql.GraphPattern, ns *voc.Namespaces) linkedql.GraphPattern {
 	context := make(map[string]interface{})
 	for _, namespace := range ns.List() {
 		context[namespace.Prefix] = namespace.Full
@@ -98,24 +134,44 @@ func parsePattern(pattern linkedql.GraphPattern, ns *voc.Namespaces) ([]quad.Qua
 	for key, value := range pattern {
 		patternClone[key] = value
 	}
-	reader := jsonld.NewReaderFromMap(patternClone)
-	quads, err := quad.ReadAll(reader)
-	if err != nil {
-		return nil, err
-	}
-	if id, ok := patternClone["@id"]; ok && len(quads) == 0 {
+	return pattern
+}
+
+func quadsFromMap(o interface{}) ([]quad.Quad, error) {
+	reader := jsonld.NewReaderFromMap(o)
+	return quad.ReadAll(reader)
+}
+
+func normalizeQuads(quads []quad.Quad, pattern linkedql.GraphPattern) ([]quad.Quad, error) {
+	if id, ok := pattern["@id"]; ok && len(quads) == 0 {
 		idString, ok := id.(string)
 		if !ok {
 			return nil, fmt.Errorf("Unexpected type for @id %T", idString)
 		}
 		quads = append(quads, makeSingleEntityQuad(quad.IRI(idString)))
 	}
+	return quads, nil
+}
+
+func parsePattern(pattern linkedql.GraphPattern, ns *voc.Namespaces) ([]quad.Quad, error) {
+	contextualizedPattern := contextualizePattern(pattern, ns)
+	quads, err := quadsFromMap(contextualizedPattern)
+	if err != nil {
+		return nil, err
+	}
+	quads, err = normalizeQuads(quads, contextualizedPattern)
+	if err != nil {
+		return nil, err
+	}
 	if len(quads) == 0 && len(pattern) != 0 {
 		return nil, fmt.Errorf("Pattern does not parse to any quad. `{}` is the only pattern allowed to not parse to any quad")
 	}
 	return quads, nil
 }
 
+// makeSingleEntityQuad creates a quad representing a propertyless entity. The
+// quad declares the entity is of type Resource, the base type of all entities
+// in RDF.
 func makeSingleEntityQuad(id quad.IRI) quad.Quad {
 	return quad.Quad{Subject: id, Predicate: quad.IRI(rdf.Type), Object: quad.IRI(rdfs.Resource)}
 }
diff --git a/query/linkedql/steps/operators.go b/query/linkedql/steps/operators.go
deleted file mode 100644
index f2f54dc..0000000
--- a/query/linkedql/steps/operators.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package steps
-
-import (
-	"regexp"
-
-	"github.com/cayleygraph/cayley/query/linkedql"
-	"github.com/cayleygraph/cayley/query/path"
-	"github.com/cayleygraph/cayley/query/shape"
-)
-
-// RegExp corresponds to regex().
-type RegExp struct {
-	Pattern     string `json:"pattern"`
-	IncludeIRIs bool   `json:"includeIRIs,omitempty"`
-}
-
-// Description implements Operator.
-func (s *RegExp) Description() string {
-	return "RegExp filters out values that do not match given pattern. If includeIRIs is set to true it matches IRIs in addition to literals."
-}
-
-// Apply implements Operator.
-func (s *RegExp) Apply(p *path.Path) (*path.Path, error) {
-	pattern, err := regexp.Compile(s.Pattern)
-	if err != nil {
-		return nil, err
-	}
-	if s.IncludeIRIs {
-		return p.RegexWithRefs(pattern), nil
-	}
-	return p.RegexWithRefs(pattern), nil
-}
-
-var _ linkedql.Operator = (*Like)(nil)
-
-// Like corresponds to like().
-type Like struct {
-	Pattern string `json:"pattern"`
-}
-
-// Description implements Operator.
-func (s *Like) Description() string {
-	return "Like filters out values that do not match given pattern."
-}
-
-// Apply implements Operator.
-func (s *Like) Apply(p *path.Path) (*path.Path, error) {
-	return p.Filters(shape.Wildcard{Pattern: s.Pattern}), nil
-}
diff --git a/query/linkedql/steps/properties.go b/query/linkedql/steps/properties.go
index beb6c09..aadc4bb 100644
--- a/query/linkedql/steps/properties.go
+++ b/query/linkedql/steps/properties.go
@@ -1,6 +1,8 @@
 package steps
 
 import (
+	"fmt"
+
 	"github.com/cayleygraph/cayley/graph"
 	"github.com/cayleygraph/cayley/query"
 	"github.com/cayleygraph/cayley/query/linkedql"
@@ -18,9 +20,8 @@ var _ linkedql.PathStep = (*Properties)(nil)
 
 // Properties corresponds to .properties().
 type Properties struct {
-	From linkedql.PathStep `json:"from"`
-	// TODO(iddan): Use linkedql.PropertyPath
-	Names []quad.IRI `json:"names"`
+	From  linkedql.PathStep      `json:"from"`
+	Names *linkedql.PropertyPath `json:"names"`
 }
 
 // Description implements Step.
@@ -34,6 +35,26 @@ func (s *Properties) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (quer
 	return linkedql.NewValueIteratorFromPathStep(s, qs, ns)
 }
 
+func resolveNames(names *linkedql.PropertyPath) (linkedql.PropertyIRIs, error) {
+	if names == nil {
+		return nil, fmt.Errorf("Not implemented: should tag all properties")
+	}
+	switch n := names.PropertyPathI.(type) {
+	case linkedql.PropertyStep:
+		return nil, fmt.Errorf("Not implemented: should use step to resolve to properties")
+	case linkedql.PropertyIRIs:
+		return n, nil
+	case linkedql.PropertyIRIStrings:
+		return n.PropertyIRIs(), nil
+	case linkedql.PropertyIRI:
+		return linkedql.PropertyIRIs{n}, nil
+	case linkedql.PropertyIRIString:
+		return linkedql.PropertyIRIs{linkedql.PropertyIRI(n)}, nil
+	default:
+		return nil, fmt.Errorf("Unexpected type")
+	}
+}
+
 // BuildPath implements linkedql.PathStep.
 func (s *Properties) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
 	fromPath, err := s.From.BuildPath(qs, ns)
@@ -41,14 +62,14 @@ func (s *Properties) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Pa
 		return nil, err
 	}
 	p := fromPath
-	if s.Names != nil {
-		for _, name := range s.Names {
-			name = name.FullWith(ns)
-			tag := string(name)
-			p = p.Save(name, tag)
-		}
-	} else {
-		panic("Not implemented: should tag all properties")
+	names, err := resolveNames(s.Names)
+	if err != nil {
+		return nil, err
+	}
+	for _, n := range names {
+		name := quad.IRI(n).FullWith(ns)
+		tag := string(name)
+		p = p.Save(name, tag)
 	}
 	return p, nil
 }
diff --git a/query/linkedql/steps/regexp.go b/query/linkedql/steps/regexp.go
new file mode 100644
index 0000000..504f459
--- /dev/null
+++ b/query/linkedql/steps/regexp.go
@@ -0,0 +1,51 @@
+package steps
+
+import (
+	"regexp"
+
+	"github.com/cayleygraph/cayley/graph"
+	"github.com/cayleygraph/cayley/query"
+	"github.com/cayleygraph/cayley/query/linkedql"
+	"github.com/cayleygraph/cayley/query/path"
+	"github.com/cayleygraph/quad/voc"
+)
+
+func init() {
+	linkedql.Register(&RegExp{})
+}
+
+var _ linkedql.IteratorStep = (*RegExp)(nil)
+var _ linkedql.PathStep = (*RegExp)(nil)
+
+// RegExp corresponds to regex().
+type RegExp struct {
+	From        linkedql.PathStep `json:"from"`
+	Expression  string            `json:"expression"`
+	IncludeIRIs bool              `json:"includeIRIs,omitempty"`
+}
+
+// Description implements Step.
+func (s *RegExp) Description() string {
+	return "RegExp filters out values that do not match given pattern. If includeIRIs is set to true it matches IRIs in addition to literals."
+}
+
+// BuildIterator implements linkedql.IteratorStep.
+func (s *RegExp) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (query.Iterator, error) {
+	return linkedql.NewValueIteratorFromPathStep(s, qs, ns)
+}
+
+// BuildPath implements PathStep.
+func (s *RegExp) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*path.Path, error) {
+	fromPath, err := s.From.BuildPath(qs, ns)
+	if err != nil {
+		return nil, err
+	}
+	pattern, err := regexp.Compile(s.Expression)
+	if err != nil {
+		return nil, err
+	}
+	if s.IncludeIRIs {
+		return fromPath.RegexWithRefs(pattern), nil
+	}
+	return fromPath.RegexWithRefs(pattern), nil
+}
diff --git a/query/linkedql/steps/reverse_properties.go b/query/linkedql/steps/reverse_properties.go
index 275b7f7..f0380ac 100644
--- a/query/linkedql/steps/reverse_properties.go
+++ b/query/linkedql/steps/reverse_properties.go
@@ -18,9 +18,8 @@ var _ linkedql.PathStep = (*ReverseProperties)(nil)
 
 // ReverseProperties corresponds to .reverseProperties().
 type ReverseProperties struct {
-	From linkedql.PathStep `json:"from"`
-	// TODO(iddan): Use property path
-	Names []quad.IRI `json:"names"`
+	From  linkedql.PathStep      `json:"from"`
+	Names *linkedql.PropertyPath `json:"names"`
 }
 
 // Description implements Step.
@@ -40,8 +39,12 @@ func (s *ReverseProperties) BuildPath(qs graph.QuadStore, ns *voc.Namespaces) (*
 		return nil, err
 	}
 	p := fromPath
-	for _, name := range s.Names {
-		name = name.FullWith(ns)
+	names, err := resolveNames(s.Names)
+	if err != nil {
+		return nil, err
+	}
+	for _, n := range names {
+		name := quad.IRI(n).FullWith(ns)
 		tag := string(name)
 		p = fromPath.SaveReverse(name, tag)
 	}
diff --git a/query/linkedql/steps/steps_final.go b/query/linkedql/steps/steps_final.go
index 166600a..8537369 100644
--- a/query/linkedql/steps/steps_final.go
+++ b/query/linkedql/steps/steps_final.go
@@ -9,8 +9,6 @@ import (
 
 func init() {
 	linkedql.Register(&Select{})
-	linkedql.Register(&SelectFirst{})
-	linkedql.Register(&Value{})
 	linkedql.Register(&Documents{})
 }
 
@@ -18,8 +16,9 @@ var _ linkedql.IteratorStep = (*Select)(nil)
 
 // Select corresponds to .select().
 type Select struct {
-	Tags []string          `json:"tags"`
-	From linkedql.PathStep `json:"from"`
+	Properties []string          `json:"properties"`
+	From       linkedql.PathStep `json:"from"`
+	ExcludeID  bool              `json:"excludeID"`
 }
 
 // Description implements Step.
@@ -33,54 +32,8 @@ func (s *Select) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (query.It
 	if err != nil {
 		return nil, err
 	}
-	return &linkedql.TagsIterator{ValueIt: valueIt, Selected: s.Tags}, nil
-}
-
-var _ linkedql.IteratorStep = (*SelectFirst)(nil)
-
-// SelectFirst corresponds to .selectFirst().
-type SelectFirst struct {
-	Tags []string          `json:"tags"`
-	From linkedql.PathStep `json:"from"`
-}
-
-// Description implements Step.
-func (s *SelectFirst) Description() string {
-	return "Like Select but only returns the first result"
-}
-
-func singleValueIteratorFromPathStep(step linkedql.PathStep, qs graph.QuadStore, ns *voc.Namespaces) (*linkedql.ValueIterator, error) {
-	p, err := step.BuildPath(qs, ns)
-	if err != nil {
-		return nil, err
-	}
-	return linkedql.NewValueIterator(p.Limit(1), qs), nil
-}
-
-// BuildIterator implements IteratorStep
-func (s *SelectFirst) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (query.Iterator, error) {
-	it, err := singleValueIteratorFromPathStep(s.From, qs, ns)
-	if err != nil {
-		return nil, err
-	}
-	return &linkedql.TagsIterator{it, s.Tags}, nil
-}
-
-var _ linkedql.IteratorStep = (*Value)(nil)
-
-// Value corresponds to .value().
-type Value struct {
-	From linkedql.PathStep `json:"from"`
-}
-
-// Description implements Step.
-func (s *Value) Description() string {
-	return "Value returns a single value matched in the query"
-}
-
-// BuildIterator implements IteratorStep
-func (s *Value) BuildIterator(qs graph.QuadStore, ns *voc.Namespaces) (query.Iterator, error) {
-	return singleValueIteratorFromPathStep(s.From, qs, ns)
+	it := linkedql.NewTagsIterator(valueIt, s.Properties, s.ExcludeID)
+	return &it, nil
 }
 
 var _ linkedql.IteratorStep = (*Documents)(nil)
diff --git a/query/linkedql/steps/visit.go b/query/linkedql/steps/visit.go
index 559213f..d9329a7 100644
--- a/query/linkedql/steps/visit.go
+++ b/query/linkedql/steps/visit.go
@@ -17,8 +17,8 @@ var _ linkedql.PathStep = (*Visit)(nil)
 
 // Visit corresponds to .view().
 type Visit struct {
-	From       linkedql.PathStep     `json:"from"`
-	Properties linkedql.PropertyPath `json:"properties"`
+	From       linkedql.PathStep      `json:"from"`
+	Properties *linkedql.PropertyPath `json:"properties"`
 }
 
 // Description implements Step.
diff --git a/query/linkedql/steps/visit_reverse.go b/query/linkedql/steps/visit_reverse.go
index c42cf45..de57a15 100644
--- a/query/linkedql/steps/visit_reverse.go
+++ b/query/linkedql/steps/visit_reverse.go
@@ -17,8 +17,8 @@ var _ linkedql.PathStep = (*VisitReverse)(nil)
 
 // VisitReverse corresponds to .viewReverse().
 type VisitReverse struct {
-	From       linkedql.PathStep     `json:"from"`
-	Properties linkedql.PropertyPath `json:"properties"`
+	From       linkedql.PathStep      `json:"from"`
+	Properties *linkedql.PropertyPath `json:"properties"`
 }
 
 // Description implements Step.
diff --git a/server/http/api_v2.go b/server/http/api_v2.go
index add917f..f33148a 100644
--- a/server/http/api_v2.go
+++ b/server/http/api_v2.go
@@ -36,6 +36,7 @@ import (
 	// Writer is imported for writers to be registered
 	_ "github.com/cayleygraph/cayley/writer"
 	"github.com/cayleygraph/quad"
+	"github.com/cayleygraph/quad/voc"
 )
 
 const (
@@ -584,3 +585,56 @@ func (api *APIv2) ServeQuery(w http.ResponseWriter, r *http.Request) {
 	}
 	writeResults(w, out)
 }
+
+// NamespaceRule defines a prefix for a namespace when prepended to the suffix of a compact IRI, results in an IRI.
+// For example rdfs is a prefix for the namespace http://www.w3.org/2000/01/rdf-schema#.
+type NamespaceRule struct {
+	Prefix    string `json:"prefix"`
+	Namespace string `json:"namespace"`
+}
+
+// getNamespaceRules returns all the registered rules
+func getNamespaceRules() []NamespaceRule {
+	var rules []NamespaceRule
+	for _, n := range voc.List() {
+		rules = append(rules, NamespaceRule{
+			Prefix:    strings.TrimSuffix(n.Prefix, ":"),
+			Namespace: n.Full,
+		})
+	}
+	return rules
+}
+
+// serveGetNamespaceRules responds with all the registered rules encoded to JSON
+func serveGetNamespaceRules(w http.ResponseWriter) {
+	rules := getNamespaceRules()
+	encoder := json.NewEncoder(w)
+	w.Header().Set(hdrContentType, contentTypeJSON)
+	w.WriteHeader(http.StatusOK)
+	encoder.Encode(rules)
+}
+
+// serveGetNamespaceRules registers received rule encoded in JSON
+func servePostNamespaceRules(w http.ResponseWriter, r *http.Request) {
+	var rule NamespaceRule
+	decoder := json.NewDecoder(r.Body)
+	decoder.Decode(&rule)
+	voc.RegisterPrefix(rule.Prefix, rule.Namespace)
+	w.WriteHeader(http.StatusCreated)
+}
+
+// ServeNamespaceRules serves requests for the namespace rules resource.
+// The resource supports getting all registered rules and registering a rule.
+// The resource wraps the quad/voc module.
+func (api *APIv2) ServeNamespaceRules(w http.ResponseWriter, r *http.Request) {
+	switch r.Method {
+	case http.MethodGet:
+		serveGetNamespaceRules(w)
+		return
+	case http.MethodPost:
+		servePostNamespaceRules(w, r)
+		return
+	default:
+		jsonResponse(w, http.StatusMethodNotAllowed, nil)
+	}
+}
diff --git a/server/http/common.go b/server/http/common.go
index 245fc40..c27ab73 100644
--- a/server/http/common.go
+++ b/server/http/common.go
@@ -6,7 +6,7 @@ import (
 	"net/http"
 
 	"github.com/cayleygraph/cayley/graph"
-	"github.com/cayleygraph/cayley/graph/http"
+	httpgraph "github.com/cayleygraph/cayley/graph/http"
 )
 
 func jsonResponse(w http.ResponseWriter, code int, err interface{}) {
@@ -27,6 +27,7 @@ func jsonResponse(w http.ResponseWriter, code int, err interface{}) {
 	w.Write([]byte(`}`))
 }
 
+// HandleForRequest returns new graph.Handle for given writer name, options and request
 func HandleForRequest(h *graph.Handle, wtyp string, wopt graph.Options, r *http.Request) (*graph.Handle, error) {
 	g, ok := h.QuadStore.(httpgraph.QuadStore)
 	if !ok {


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..e187fe5
--- /dev/null
+++ b/query/linkedql/steps/jsonld_util_test.go
@@ -0,0 +1,136 @@
+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 multiple values but instead received the single value: \"Alice\""),
+	},
+	{
+		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,
+	},
+	{
+		name:     "Single matching properties with @value string array string",
+		source:   map[string]interface{}{"http://example.com/name": []interface{}{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 array",
+		source:   map[string]interface{}{"http://example.com/name": "Alice"},
+		target:   map[string]interface{}{"http://example.com/name": []interface{}{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/match_test.go b/query/linkedql/steps/match_test.go
new file mode 100644
index 0000000..16ba906
--- /dev/null
+++ b/query/linkedql/steps/match_test.go
@@ -0,0 +1,103 @@
+package steps
+
+import (
+	"testing"
+
+	"github.com/cayleygraph/cayley/query/path"
+	"github.com/cayleygraph/quad"
+	"github.com/cayleygraph/quad/voc"
+	"github.com/stretchr/testify/require"
+)
+
+var (
+	ns      = "http://example.com/"
+	alice   = quad.IRI(ns + "alice")
+	likes   = quad.IRI(ns + "likes")
+	name    = quad.IRI(ns + "name")
+	bob     = quad.IRI(ns + "bob")
+	address = quad.IRI(ns + "address")
+	city    = quad.IRI(ns + "city")
+	street  = quad.IRI(ns + "street")
+	country = quad.IRI(ns + "country")
+)
+
+var patternTestCases = []struct {
+	name     string
+	pattern  map[string]interface{}
+	expected *path.Path
+}{
+	{
+		name:     "Empty Pattern",
+		pattern:  map[string]interface{}{},
+		expected: path.StartMorphism(),
+	},
+	{
+		name: "Single Entity",
+		pattern: map[string]interface{}{
+			"@id": string(alice),
+		},
+		expected: path.StartMorphism().Is(alice),
+	},
+	{
+		name: "Single Property Value",
+		pattern: map[string]interface{}{
+			string(likes): map[string]interface{}{"@id": string(bob)},
+		},
+		expected: path.StartMorphism().Has(likes, bob),
+	},
+	{
+		name: "Nested Structure",
+		pattern: map[string]interface{}{
+			string(address): map[string]interface{}{
+				string(street): "Lafayette",
+			},
+		},
+		expected: path.
+			StartMorphism().
+			Out(address).
+			Follow(
+				path.StartMorphism().
+					Has(street, quad.String("Lafayette")),
+			).
+			Back(""),
+	},
+	{
+		name: "Two Level Nested Structure",
+		pattern: map[string]interface{}{
+			string(address): map[string]interface{}{
+				string(country): map[string]interface{}{
+					string(name): "The United States of America",
+				},
+			},
+		},
+		expected: path.
+			StartMorphism().
+			Out(address).
+			Follow(
+				path.StartMorphism().
+					Out(country).
+					Follow(
+						path.StartMorphism().
+							Has(name, quad.String("The United States of America")),
+					).
+					Back(""),
+			).
+			Back(""),
+	},
+}
+
+func TestBuildPath(t *testing.T) {
+	for _, c := range patternTestCases {
+		t.Run(c.name, func(t *testing.T) {
+			ns := voc.Namespaces{}
+			quads, err := parsePattern(c.pattern, &ns)
+			require.NoError(t, err)
+			p := buildPatternPath(quads, &ns)
+			expectedShape := c.expected.Shape()
+			shape := p.Shape()
+			// TODO(iddan): replace with stable comparison. Currently, it breaks
+			// because order of properties is not guaranteed
+			require.Equal(t, expectedShape, shape)
+		})
+	}
+}
diff --git a/query/linkedql/steps/steps_test.go b/query/linkedql/steps/steps_test.go
index 99aea0d..0f68601 100644
--- a/query/linkedql/steps/steps_test.go
+++ b/query/linkedql/steps/steps_test.go
@@ -1,646 +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/stretchr/testify/require"
 )
 
-var singleQuadData = []quad.Quad{
-	quad.MakeIRI("alice", "likes", "bob", ""),
+type TestCase struct {
+	Data    interface{} `json:"data"`
+	Query   interface{} `json:"query"`
+	Results interface{} `json:"results"`
 }
 
-var testCases = []struct {
-	name    string
-	data    []quad.Quad
-	query   linkedql.IteratorStep
-	results []interface{}
-}{
-	{
-		name:  "All Entities",
-		data:  singleQuadData,
-		query: &Entities{Identifiers: []linkedql.EntityIdentifier{linkedql.EntityIdentifierString("alice")}},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-		},
-	},
-	{
-		name:  "All Vertices",
-		data:  singleQuadData,
-		query: &Vertex{Values: nil},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-			map[string]string{"@id": "likes"},
-			map[string]string{"@id": "bob"},
-		},
-	},
-	{
-		name: "Select",
-		data: singleQuadData,
-		query: &Select{
-			From: &As{
-				From: &Visit{
-					From: &As{
-						From: &Vertex{},
-						Name: "liker",
-					},
-					Properties: linkedql.PropertyPath{&Vertex{Values: []quad.Value{quad.IRI("likes")}}},
-				},
-				Name: "liked",
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				"liker": map[string]string{"@id": "alice"},
-				"liked": map[string]string{"@id": "bob"},
-			},
-		},
-	},
-	{
-		name: "Select with tags",
-		data: singleQuadData,
-		query: &Select{
-			Tags: []string{"liker"},
-			From: &As{
-				From: &Visit{
-					From: &As{
-						From: &Vertex{},
-						Name: "liker",
-					},
-					Properties: linkedql.PropertyPath{&Vertex{Values: []quad.Value{quad.IRI("likes")}}},
-				},
-				Name: "liked",
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				"liker": map[string]string{"@id": "alice"},
-			},
-		},
-	},
-	{
-		name: "Back",
-		data: singleQuadData,
-		query: &Back{
-			From: &Visit{
-				From: &Vertex{
-					Values: []quad.Value{quad.IRI("alice")},
-				},
-				Properties: linkedql.PropertyPath{&Vertex{
-					Values: []quad.Value{
-						quad.IRI("likes"),
-					},
-				}},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-		},
-	},
-	{
-		name: "Both",
-		data: []quad.Quad{
-			quad.MakeIRI("alice", "likes", "bob", ""),
-			quad.MakeIRI("bob", "likes", "dan", ""),
-		},
-		query: &Both{
-			From: &Vertex{
-				Values: []quad.Value{quad.IRI("bob")},
-			},
-			Properties: linkedql.PropertyPath{&Vertex{Values: []quad.Value{quad.IRI("likes")}}},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-			map[string]string{"@id": "dan"},
-		},
-	},
-	{
-		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{quad.IRI("alice"), quad.IRI("likes")},
-			},
-			Steps: []linkedql.PathStep{
-				&Vertex{
-					Values: []quad.Value{quad.IRI("likes")},
-				},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-		},
-	},
-	{
-		name: "Filter RegExp",
-		data: []quad.Quad{
-			{Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.String("Alice"), Label: nil},
-		},
-		query: &Filter{
-			From:   &Vertex{Values: []quad.Value{}},
-			Filter: &RegExp{Pattern: "A"},
-		},
-		results: []interface{}{
-			"Alice",
-		},
-	},
-	{
-		name: "Filter Like",
-		data: []quad.Quad{
-			{Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.String("Alice"), Label: nil},
-		},
-		query: &Filter{
-			From:   &Vertex{Values: []quad.Value{}},
-			Filter: &Like{Pattern: "a%"},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-		},
-	},
-	{
-		name: "Filter LessThan",
-		data: []quad.Quad{
-			{Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(0), Label: nil},
-			{Subject: quad.IRI("alice"), Predicate: quad.IRI("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: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(0), Label: nil},
-			{Subject: quad.IRI("alice"), Predicate: quad.IRI("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: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(-1), Label: nil},
-			{Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(0), Label: nil},
-			{Subject: quad.IRI("alice"), Predicate: quad.IRI("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: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(0), Label: nil},
-			{Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(1), Label: nil},
-			{Subject: quad.IRI("alice"), Predicate: quad.IRI("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.PropertyPath{&Vertex{
-				Values: []quad.Value{quad.IRI("likes")},
-			}},
-			Values: []quad.Value{quad.IRI("bob")},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-		},
-	},
-	{
-		name: "HasReverse",
-		data: singleQuadData,
-		query: &HasReverse{
-			From: &Vertex{
-				Values: []quad.Value{},
-			},
-			Property: linkedql.PropertyPath{&Vertex{
-				Values: []quad.Value{quad.IRI("likes")},
-			}},
-			Values: []quad.Value{quad.IRI("alice")},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "bob"},
-		},
-	},
-	{
-		name: "ViewReverse",
-		data: singleQuadData,
-		query: &VisitReverse{
-			From:       &Vertex{Values: []quad.Value{}},
-			Properties: linkedql.PropertyPath{&Vertex{Values: []quad.Value{quad.IRI("likes")}}},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-		},
-	},
-	{
-		name: "PropertyNames",
-		data: singleQuadData,
-		query: &PropertyNames{
-			From: &Vertex{Values: []quad.Value{}},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "likes"},
-		},
-	},
-	{
-		name: "Intersect",
-		data: []quad.Quad{
-			quad.MakeIRI("bob", "likes", "alice", ""),
-			quad.MakeIRI("dani", "likes", "alice", ""),
-		},
-		query: &Intersect{
-			From: &Visit{
-				From: &Vertex{Values: []quad.Value{quad.IRI("bob")}},
-				Properties: linkedql.PropertyPath{&Vertex{
-					Values: []quad.Value{quad.IRI("likes")},
-				}},
-			},
-			Steps: []linkedql.PathStep{
-				&Visit{
-					From:       &Vertex{Values: []quad.Value{quad.IRI("bob")}},
-					Properties: linkedql.PropertyPath{&Vertex{Values: []quad.Value{quad.IRI("likes")}}},
-				},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-		},
-	},
-	{
-		name: "Is",
-		data: singleQuadData,
-		query: &Is{
-			Values: []quad.Value{quad.IRI("bob")},
-			From: &Visit{
-				From: &Vertex{Values: []quad.Value{quad.IRI("alice")}},
-				Properties: linkedql.PropertyPath{&Vertex{
-					Values: []quad.Value{quad.IRI("likes")},
-				}},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "bob"},
-		},
-	},
-	{
-		name: "Limit",
-		data: singleQuadData,
-		query: &Limit{
-			Limit: 2,
-			From: &Vertex{
-				Values: []quad.Value{},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-			map[string]string{"@id": "likes"},
-		},
-	},
-	{
-		name: "View",
-		data: singleQuadData,
-		query: &Visit{
-			From:       &Vertex{Values: []quad.Value{}},
-			Properties: linkedql.PropertyPath{&Vertex{Values: []quad.Value{quad.IRI("likes")}}},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "bob"},
-		},
-	},
-	{
-		name: "PropertyNames",
-		data: singleQuadData,
-		query: &PropertyNames{
-			From: &Vertex{Values: []quad.Value{}},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "likes"},
-		},
-	},
-	{
-		name: "Properties",
-		data: singleQuadData,
-		query: &Select{
-			From: &Properties{
-				From:  &Vertex{Values: []quad.Value{}},
-				Names: []quad.IRI{quad.IRI("likes")},
-			},
-		},
-		results: []interface{}{map[string]interface{}{"likes": map[string]string{"@id": "bob"}}},
-	},
-	{
-		name: "ReversePropertyNamesAs",
-		data: singleQuadData,
-		query: &Select{
-			From: &ReversePropertyNamesAs{
-				From: &Vertex{Values: []quad.Value{}},
-				Tag:  "predicate",
-			},
-		},
-		results: []interface{}{map[string]interface{}{"predicate": map[string]string{"@id": "likes"}}},
-	},
-	{
-		name: "PropertyNamesAs",
-		data: singleQuadData,
-		query: &Select{
-			From: &PropertyNamesAs{
-				From: &Vertex{Values: []quad.Value{}},
-				Tag:  "predicate",
-			},
-		},
-		results: []interface{}{map[string]interface{}{"predicate": map[string]string{"@id": "likes"}}},
-	},
-	{
-		name: "ReverseProperties",
-		data: singleQuadData,
-		query: &Select{
-			From: &ReverseProperties{
-				From:  &Vertex{Values: []quad.Value{}},
-				Names: []quad.IRI{quad.IRI("likes")},
-			},
-		},
-		results: []interface{}{map[string]interface{}{"likes": map[string]string{"@id": "alice"}}},
-	},
-	{
-		name: "Skip",
-		data: singleQuadData,
-		query: &Skip{
-			Offset: 2,
-			From: &Vertex{
-				Values: []quad.Value{},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "bob"},
-		},
-	},
-	{
-		name: "Union",
-		data: singleQuadData,
-		query: &Union{
-			From: &Vertex{
-				Values: []quad.Value{quad.IRI("alice")},
-			},
-			Steps: []linkedql.PathStep{
-				&Vertex{
-					Values: []quad.Value{quad.IRI("bob")},
-				},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-			map[string]string{"@id": "bob"},
-		},
-	},
-	{
-		name: "SelectFirst",
-		data: singleQuadData,
-		query: &SelectFirst{
-			From: &As{
-				From: &Visit{
-					From: &As{
-						Name: "liker",
-						From: &Vertex{},
-					},
-					Properties: linkedql.PropertyPath{&Vertex{Values: []quad.Value{quad.IRI("likes")}}},
-				},
-				Name: "liked",
-			},
-		},
-		results: []interface{}{map[string]interface{}{"liked": map[string]string{"@id": "bob"}, "liker": map[string]string{"@id": "alice"}}},
-	},
-	{
-		name: "Unique",
-		data: singleQuadData,
-		query: &Unique{
-			From: &Vertex{
-				Values: []quad.Value{quad.IRI("alice"), quad.IRI("alice"), quad.IRI("bob")},
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-			map[string]string{"@id": "bob"},
-		},
-	},
-	{
-		name: "Order",
-		data: singleQuadData,
-		query: &Order{
-			From: &Vertex{},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "alice"},
-			map[string]string{"@id": "bob"},
-			map[string]string{"@id": "likes"},
-		},
-	},
-	{
-		name: "Optional",
-		data: []quad.Quad{
-			quad.MakeIRI("alice", "likes", "bob", ""),
-			quad.MakeIRI("alice", "name", "Alice", ""),
-			quad.MakeIRI("bob", "name", "Bob", ""),
-		},
-		query: &Select{
-			From: &Optional{
-				From: &Properties{
-					From:  &Vertex{Values: []quad.Value{}},
-					Names: []quad.IRI{quad.IRI("name")},
-				},
-				Step: &Properties{
-					From:  &Placeholder{},
-					Names: []quad.IRI{quad.IRI("likes")},
-				},
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				"likes": map[string]string{"@id": "bob"},
-				"name":  map[string]string{"@id": "Alice"},
-			},
-			map[string]interface{}{
-				"name": map[string]string{"@id": "Bob"},
-			},
-		},
-	},
-	{
-		name: "Where",
-		data: []quad.Quad{
-			quad.MakeIRI("alice", "likes", "bob", ""),
-			quad.MakeIRI("alice", "name", "Alice", ""),
-			quad.MakeIRI("bob", "name", "Bob", ""),
-		},
-		query: &Select{
-			From: &As{
-				From: &Where{
-					From: &Vertex{},
-					Steps: []linkedql.PathStep{
-						&As{
-							From: &Visit{
-								From: &Visit{
-									From:       &Placeholder{},
-									Properties: linkedql.PropertyPath{&Vertex{Values: []quad.Value{quad.IRI("likes")}}},
-								},
-								Properties: linkedql.PropertyPath{&Vertex{Values: []quad.Value{quad.IRI("name")}}},
-							},
-							Name: "likesName",
-						},
-						&As{
-							From: &Visit{
-								From:       &Placeholder{},
-								Properties: linkedql.PropertyPath{&Vertex{Values: []quad.Value{quad.IRI("name")}}},
-							},
-							Name: "name",
-						},
-					},
-				},
-				Name: "person",
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				"person":    map[string]string{"@id": "alice"},
-				"name":      map[string]string{"@id": "Alice"},
-				"likesName": map[string]string{"@id": "Bob"},
-			},
-		},
-	},
-	{
-		name: "Documents",
-		data: []quad.Quad{
-			quad.MakeIRI("alice", "likes", "bob", ""),
-			quad.MakeIRI("alice", "name", "Alice", ""),
-			quad.MakeIRI("bob", "name", "Bob", ""),
-			quad.MakeIRI("bob", "likes", "alice", ""),
-		},
-		query: &Documents{
-			From: &Properties{
-				From:  &Vertex{Values: []quad.Value{}},
-				Names: []quad.IRI{quad.IRI("name"), quad.IRI("likes")},
-			},
-		},
-		results: []interface{}{
-			map[string]interface{}{
-				"@id":   "alice",
-				"name":  []interface{}{map[string]string{"@id": "Alice"}},
-				"likes": []interface{}{map[string]string{"@id": "bob"}},
-			},
-			map[string]interface{}{
-				"@id":   "bob",
-				"name":  []interface{}{map[string]string{"@id": "Bob"}},
-				"likes": []interface{}{map[string]string{"@id": "alice"}},
-			},
-		},
-	},
-	{
-		name: "Context",
-		data: []quad.Quad{
-			quad.MakeIRI("http://example.org/alice", "http://example.org/likes", "http://example.org/bob", ""),
-			quad.MakeIRI("http://example.org/bob", "http://example.org/likes", "http://example.org/alice", ""),
-		},
-		query: &Context{
-			From: &Has{
-				From:     &Vertex{},
-				Property: linkedql.PropertyPath{linkedql.PropertyIRI("likes")},
-				Values:   []quad.Value{quad.IRI("bob")},
-			},
-			Rules: map[string]string{
-				"bob":   "http://example.org/bob",
-				"likes": "http://example.org/likes",
-			},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "http://example.org/alice"},
-		},
-	},
-	{
-		name: "Match @id",
-		data: []quad.Quad{
-			quad.MakeIRI("http://example.org/alice", "http://example.org/likes", "http://example.org/bob", ""),
-			quad.MakeIRI("http://example.org/bob", "http://example.org/likes", "http://example.org/alice", ""),
-		},
-		query: &Match{
-			From:    &Vertex{},
-			Pattern: linkedql.GraphPattern{"@id": "http://example.org/alice"},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "http://example.org/alice"},
-		},
-	},
-	{
-		name: "Match property",
-		data: []quad.Quad{
-			quad.MakeIRI("http://example.org/alice", "http://example.org/likes", "http://example.org/bob", ""),
-			quad.MakeIRI("http://example.org/bob", "http://example.org/likes", "http://example.org/alice", ""),
-		},
-		query: &Match{
-			From:    &Vertex{},
-			Pattern: linkedql.GraphPattern{"http://example.org/likes": map[string]interface{}{"@id": "http://example.org/alice"}},
-		},
-		results: []interface{}{
-			map[string]string{"@id": "http://example.org/bob"},
-		},
-	},
-	// FIXME(iddan): add test for match nested objects.
+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
+}
+
+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..61f0902
--- /dev/null
+++ b/query/linkedql/steps/test-cases/Documents.json
@@ -0,0 +1,58 @@
+{
+  "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": {
+    "@graph": [
+      {
+        "@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/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-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/collect.json b/query/linkedql/steps/test-cases/collect.json
new file mode 100644
index 0000000..28346b9
--- /dev/null
+++ b/query/linkedql/steps/test-cases/collect.json
@@ -0,0 +1,34 @@
+{
+  "data": {
+    "http://example.com/friends": {
+      "@list": [
+        { "@id": "http://example.com/alice" },
+        { "@id": "http://example.com/bob" }
+      ]
+    }
+  },
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Documents",
+    "linkedql:from": {
+      "@type": "linkedql:Collect",
+      "linkedql:name": "http://example.com/friends",
+      "linkedql:from": {
+        "@type": "linkedql:Vertex",
+        "linkedql:values": []
+      }
+    }
+  },
+  "results": [
+    {
+      "http://example.com/friends": {
+        "@list": [
+          { "@id": "http://example.com/alice" },
+          { "@id": "http://example.com/bob" }
+        ]
+      }
+    }
+  ]
+}
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..61f0902
--- /dev/null
+++ b/query/linkedql/steps/test-cases/documents.json
@@ -0,0 +1,58 @@
+{
+  "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": {
+    "@graph": [
+      {
+        "@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/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/match-exact.json b/query/linkedql/steps/test-cases/match-exact.json
new file mode 100644
index 0000000..ae01a86
--- /dev/null
+++ b/query/linkedql/steps/test-cases/match-exact.json
@@ -0,0 +1,26 @@
+{
+  "data": [
+    {
+      "@id": "http://example.com/alice",
+      "http://example.com/likes": [
+        {
+          "@id": "http://example.com/bob"
+        }
+      ]
+    }
+  ],
+  "query": {
+    "@context": {
+      "linkedql": "http://cayley.io/linkedql"
+    },
+    "@type": "linkedql:Match",
+    "linkedql:pattern": {
+      "@id": "http://example.com/alice"
+    }
+  },
+  "results": [
+    {
+      "@id": "http://example.com/alice"
+    }
+  ]
+}
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-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"
+    }
+  ]
+}
diff --git a/server/http/api_v2_test.go b/server/http/api_v2_test.go
index be996e1..33a38dc 100644
--- a/server/http/api_v2_test.go
+++ b/server/http/api_v2_test.go
@@ -21,7 +21,10 @@ func makeHandle(t testing.TB, quads ...quad.Quad) *graph.Handle {
 	qs := memstore.New(quads...)
 	wr, err := writer.NewSingleReplication(qs, nil)
 	require.NoError(t, err)
-	return &graph.Handle{qs, wr}
+	return &graph.Handle{
+		QuadStore:  qs,
+		QuadWriter: wr,
+	}
 }
 
 func makeServerV2(t testing.TB, quads ...quad.Quad) *APIv2 {
@@ -62,7 +65,7 @@ func TestV2Write(t *testing.T) {
 	handler := http.HandlerFunc(api.ServeWrite)
 	handler.ServeHTTP(rr, req)
 
-	require.Equal(t, rr.Code, http.StatusOK, rr.Body.String())
+	require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
 
 	expectedResponse := newWriteResponse(len(quads))
 
@@ -84,7 +87,7 @@ func TestV2Read(t *testing.T) {
 	handler := http.HandlerFunc(api.ServeRead)
 	handler.ServeHTTP(rr, req)
 
-	require.Equal(t, rr.Code, http.StatusOK, rr.Body.String())
+	require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
 
 	reader := jsonld.NewReader(rr.Body)
 	receivedQuads, err := quad.ReadAll(reader)
@@ -108,5 +111,53 @@ func TestV2Delete(t *testing.T) {
 	handler := http.HandlerFunc(api.ServeDelete)
 	handler.ServeHTTP(rr, req)
 
-	require.Equal(t, rr.Code, http.StatusOK, rr.Body.String())
+	require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
+}
+
+func TestV2GetNamespaceRules(t *testing.T) {
+	api := makeServerV2(t)
+	buf := bytes.NewBuffer(nil)
+	req, err := http.NewRequest(http.MethodGet, prefix+"/namespace-rules", buf)
+	require.NoError(t, err)
+	rr := httptest.NewRecorder()
+	handler := http.HandlerFunc(api.ServeNamespaceRules)
+	handler.ServeHTTP(rr, req)
+	var rules []NamespaceRule
+	err = json.Unmarshal(rr.Body.Bytes(), &rules)
+	require.NoError(t, err)
+	require.Equal(t, http.StatusOK, rr.Code)
+	require.Equal(t, contentTypeJSON, rr.Header().Get(hdrContentType))
+	require.NotEmpty(t, rules)
+}
+
+func TestV2PostNamespaceRules(t *testing.T) {
+	api := makeServerV2(t)
+	rule := NamespaceRule{
+		Prefix:    "foaf",
+		Namespace: "http://xmlns.com/foaf/0.1/",
+	}
+	buf := bytes.NewBuffer(nil)
+	encoder := json.NewEncoder(buf)
+	encoder.Encode(rule)
+	req, err := http.NewRequest(http.MethodPost, prefix+"/namespace-rules", buf)
+	require.NoError(t, err)
+	rr := httptest.NewRecorder()
+	handler := http.HandlerFunc(api.ServeNamespaceRules)
+	handler.ServeHTTP(rr, req)
+	require.NoError(t, err)
+	require.Equal(t, http.StatusCreated, rr.Code)
+	require.Empty(t, rr.Body.Bytes())
+
+	// Check effect
+	req, err = http.NewRequest(http.MethodGet, prefix+"/namespace-rules", buf)
+	require.NoError(t, err)
+	rr = httptest.NewRecorder()
+	handler = http.HandlerFunc(api.ServeNamespaceRules)
+	handler.ServeHTTP(rr, req)
+	var rules []NamespaceRule
+	err = json.Unmarshal(rr.Body.Bytes(), &rules)
+	require.NoError(t, err)
+	require.Equal(t, http.StatusOK, rr.Code)
+	require.Equal(t, contentTypeJSON, rr.Header().Get(hdrContentType))
+	require.Contains(t, rules, rule)
 }