Repository: solid/community-server
Pull request #34 (2020-07-22 11:57:08)

SPARQL updates
This adds support for SPARQL updates through PATCH requests. Currently only deletes/inserts without variables are supported (will need to look into an internal querying engine potentially for more advanced queries).

The main "special" thing is probably how the input body is handled in a Patch. To prevent multiple parsings, the body parser immediately reads the body and parses it into the algebra. The data field then throws an error if it would still be accessed afterwards (which should never happen since every class that uses a Patch would know of the interface). This was already somewhat discussed with @RubenVerborgh but this can still change of course. 


Patch:
diff --git a/bin/server.ts b/bin/server.ts
index 7928f876f..cce5161ad 100644
--- a/bin/server.ts
+++ b/bin/server.ts
@@ -2,21 +2,29 @@ import yargs from 'yargs';
 import {
   AcceptPreferenceParser,
   AuthenticatedLdpHandler,
+  BodyParser,
   CompositeAsyncHandler,
   ExpressHttpServer,
+  HttpRequest,
   Operation,
+  PatchingStore,
+  Representation,
   ResponseDescription,
   SimpleAuthorizer,
   SimpleBodyParser,
   SimpleCredentialsExtractor,
   SimpleDeleteOperationHandler,
   SimpleGetOperationHandler,
+  SimplePatchOperationHandler,
   SimplePermissionsExtractor,
   SimplePostOperationHandler,
   SimpleRequestParser,
   SimpleResourceStore,
   SimpleResponseWriter,
+  SimpleSparqlUpdateBodyParser,
+  SimpleSparqlUpdatePatchHandler,
   SimpleTargetExtractor,
+  SingleThreadedResourceLocker,
 } from '..';
 
 const { argv } = yargs
@@ -29,10 +37,14 @@ const { argv } = yargs
 const { port } = argv;
 
 // This is instead of the dependency injection that still needs to be added
+const bodyParser: BodyParser = new CompositeAsyncHandler<HttpRequest, Representation>([
+  new SimpleBodyParser(),
+  new SimpleSparqlUpdateBodyParser(),
+]);
 const requestParser = new SimpleRequestParser({
   targetExtractor: new SimpleTargetExtractor(),
   preferenceParser: new AcceptPreferenceParser(),
-  bodyParser: new SimpleBodyParser(),
+  bodyParser,
 });
 
 const credentialsExtractor = new SimpleCredentialsExtractor();
@@ -41,10 +53,15 @@ const authorizer = new SimpleAuthorizer();
 
 // Will have to see how to best handle this
 const store = new SimpleResourceStore(`http://localhost:${port}/`);
+const locker = new SingleThreadedResourceLocker();
+const patcher = new SimpleSparqlUpdatePatchHandler(store, locker);
+const patchingStore = new PatchingStore(store, patcher);
+
 const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
-  new SimpleGetOperationHandler(store),
-  new SimplePostOperationHandler(store),
-  new SimpleDeleteOperationHandler(store),
+  new SimpleDeleteOperationHandler(patchingStore),
+  new SimpleGetOperationHandler(patchingStore),
+  new SimplePatchOperationHandler(patchingStore),
+  new SimplePostOperationHandler(patchingStore),
 ]);
 
 const responseWriter = new SimpleResponseWriter();
diff --git a/index.ts b/index.ts
index 871778ceb..04cea1ac1 100644
--- a/index.ts
+++ b/index.ts
@@ -17,7 +17,9 @@ export * from './src/ldp/http/ResponseWriter';
 export * from './src/ldp/http/SimpleBodyParser';
 export * from './src/ldp/http/SimpleRequestParser';
 export * from './src/ldp/http/SimpleResponseWriter';
+export * from './src/ldp/http/SimpleSparqlUpdateBodyParser';
 export * from './src/ldp/http/SimpleTargetExtractor';
+export * from './src/ldp/http/SparqlUpdatePatch';
 export * from './src/ldp/http/TargetExtractor';
 
 // LDP/Operations
@@ -26,6 +28,7 @@ export * from './src/ldp/operations/OperationHandler';
 export * from './src/ldp/operations/ResponseDescription';
 export * from './src/ldp/operations/SimpleDeleteOperationHandler';
 export * from './src/ldp/operations/SimpleGetOperationHandler';
+export * from './src/ldp/operations/SimplePatchOperationHandler';
 export * from './src/ldp/operations/SimplePostOperationHandler';
 
 // LDP/Permissions
@@ -52,11 +55,16 @@ export * from './src/server/HttpHandler';
 export * from './src/server/HttpRequest';
 export * from './src/server/HttpResponse';
 
+// Storage/Patch
+export * from './src/storage/patch/PatchHandler';
+export * from './src/storage/patch/SimpleSparqlUpdatePatchHandler';
+
 // Storage
 export * from './src/storage/AtomicResourceStore';
 export * from './src/storage/Conditions';
 export * from './src/storage/Lock';
 export * from './src/storage/LockingResourceStore';
+export * from './src/storage/PatchingStore';
 export * from './src/storage/RepresentationConverter';
 export * from './src/storage/ResourceLocker';
 export * from './src/storage/ResourceMapper';
diff --git a/package-lock.json b/package-lock.json
index 13a234b3e..894aa8156 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3080,8 +3080,7 @@
     "fast-deep-equal": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
-      "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
-      "dev": true
+      "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
     },
     "fast-json-stable-stringify": {
       "version": "2.1.0",
@@ -4747,8 +4746,7 @@
     "lodash.uniqwith": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz",
-      "integrity": "sha1-egy/ZfQ7WShiWp1NDcVLGMrcfvM=",
-      "dev": true
+      "integrity": "sha1-egy/ZfQ7WShiWp1NDcVLGMrcfvM="
     },
     "lodash.zip": {
       "version": "4.2.0",
@@ -4889,8 +4887,7 @@
     "minimist": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
-      "dev": true
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
     },
     "mixin-deep": {
       "version": "1.3.2",
@@ -5672,7 +5669,6 @@
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/rdf-string/-/rdf-string-1.4.2.tgz",
       "integrity": "sha512-74yYjS0W4N3nYDGbXBZrNsqDmhBTjqChTETO9heC2G2M3iMYaIPtEfUikNsBWUj4+4bIKyqL7vAntWBTfJpFFA==",
-      "dev": true,
       "requires": {
         "@rdfjs/data-model": "^1.1.1"
       }
@@ -5681,7 +5677,6 @@
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/rdf-terms/-/rdf-terms-1.5.1.tgz",
       "integrity": "sha512-dDhpUYxTAOWKT3Ln93A5k5UB5SftG8bPAzeZEjGeP4e7eboMhITUTDks8HDmUt9X1P+HpfnY/o6VSTSpf3Advw==",
-      "dev": true,
       "requires": {
         "@rdfjs/data-model": "^1.1.1",
         "lodash.uniqwith": "^4.5.0"
@@ -6551,6 +6546,36 @@
       "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
       "dev": true
     },
+    "sparqlalgebrajs": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-2.3.1.tgz",
+      "integrity": "sha512-zJ9EEX2BtHjdiSQoWP7fKt2IqD4zi24n3CQtRGOIzh8Hyj2JFMtje68CweaIJgQLN5kNYpNx/VT2F6nxpeIRJg==",
+      "requires": {
+        "@rdfjs/data-model": "^1.1.2",
+        "fast-deep-equal": "^3.1.1",
+        "minimist": "^1.2.5",
+        "rdf-string": "^1.3.1",
+        "sparqljs": "^3.0.1"
+      }
+    },
+    "sparqljs": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.0.2.tgz",
+      "integrity": "sha512-qmXKWx0ENeLUlvo0tgzcVfnvZnOBkyySvysIf6Y3HgWFKmu1bSrDB9K8siQYJO7jnSihoebtAPv89jvA6by/iw==",
+      "requires": {
+        "n3": "^1.5.0"
+      },
+      "dependencies": {
+        "n3": {
+          "version": "1.5.0",
+          "resolved": "https://registry.npmjs.org/n3/-/n3-1.5.0.tgz",
+          "integrity": "sha512-k4R/EOMnnRYFt+hXgqyKOHjzmshaLuHUFgrz5nsp9nAojCZuAHrro/DsIM2tS0Bgx6ed7DM5Ks3q2teJ8n7HnQ==",
+          "requires": {
+            "queue-microtask": "^1.1.2"
+          }
+        }
+      }
+    },
     "spdx-correct": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
diff --git a/package.json b/package.json
index 8efabca90..6ac7d2a34 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,8 @@
     "cors": "^2.8.5",
     "express": "^4.17.1",
     "n3": "^1.4.0",
+    "rdf-terms": "^1.5.1",
+    "sparqlalgebrajs": "^2.3.1",
     "yargs": "^15.4.1"
   },
   "devDependencies": {
diff --git a/src/ldp/http/Patch.ts b/src/ldp/http/Patch.ts
index b5a13a73e..47724298d 100644
--- a/src/ldp/http/Patch.ts
+++ b/src/ldp/http/Patch.ts
@@ -3,4 +3,9 @@ import { Representation } from '../representation/Representation';
 /**
  * Represents the changes needed for a PATCH request.
  */
-export interface Patch extends Representation {}
+export interface Patch extends Representation {
+  /**
+   * The raw body of the PATCH request.
+   */
+  raw: string;
+}
diff --git a/src/ldp/http/SimpleSparqlUpdateBodyParser.ts b/src/ldp/http/SimpleSparqlUpdateBodyParser.ts
new file mode 100644
index 000000000..22b73919f
--- /dev/null
+++ b/src/ldp/http/SimpleSparqlUpdateBodyParser.ts
@@ -0,0 +1,47 @@
+import { BodyParser } from './BodyParser';
+import { HttpRequest } from '../../server/HttpRequest';
+import { Readable } from 'stream';
+import { readableToString } from '../../util/Util';
+import { SparqlUpdatePatch } from './SparqlUpdatePatch';
+import { translate } from 'sparqlalgebrajs';
+import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
+import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
+
+/**
+ * {@link BodyParser} that supports `application/sparql-update` content.
+ * Will convert the incoming update string to algebra in a {@link SparqlUpdatePatch}.
+ * Simple since metadata parsing is not yet implemented.
+ */
+export class SimpleSparqlUpdateBodyParser extends BodyParser {
+  public async canHandle(input: HttpRequest): Promise<void> {
+    const contentType = input.headers['content-type'];
+
+    if (!contentType || contentType !== 'application/sparql-update') {
+      throw new UnsupportedMediaTypeHttpError('This parser only supports SPARQL UPDATE data.');
+    }
+  }
+
+  public async handle(input: HttpRequest): Promise<SparqlUpdatePatch> {
+    try {
+      const sparql = await readableToString(input);
+      const algebra = translate(sparql, { quads: true });
+
+      // Prevent body from being requested again
+      return {
+        algebra,
+        dataType: 'sparql-algebra',
+        raw: sparql,
+        get data(): Readable {
+          throw new Error('Body already parsed');
+        },
+        metadata: {
+          raw: [],
+          profiles: [],
+          contentType: 'application/sparql-update',
+        },
+      };
+    } catch (error) {
+      throw new UnsupportedHttpError(error);
+    }
+  }
+}
diff --git a/src/ldp/http/SparqlUpdatePatch.ts b/src/ldp/http/SparqlUpdatePatch.ts
new file mode 100644
index 000000000..1165df0b5
--- /dev/null
+++ b/src/ldp/http/SparqlUpdatePatch.ts
@@ -0,0 +1,12 @@
+import { Patch } from './Patch';
+import { Update } from 'sparqlalgebrajs/lib/algebra';
+
+/**
+ * A specific type of {@link Patch} corresponding to a SPARQL update.
+ */
+export interface SparqlUpdatePatch extends Patch {
+  /**
+   * Algebra corresponding to the SPARQL update.
+   */
+  algebra: Update;
+}
diff --git a/src/ldp/operations/SimplePatchOperationHandler.ts b/src/ldp/operations/SimplePatchOperationHandler.ts
new file mode 100644
index 000000000..532ed5281
--- /dev/null
+++ b/src/ldp/operations/SimplePatchOperationHandler.ts
@@ -0,0 +1,26 @@
+import { Operation } from './Operation';
+import { OperationHandler } from './OperationHandler';
+import { Patch } from '../http/Patch';
+import { ResourceStore } from '../../storage/ResourceStore';
+import { ResponseDescription } from './ResponseDescription';
+import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
+
+export class SimplePatchOperationHandler extends OperationHandler {
+  private readonly store: ResourceStore;
+
+  public constructor(store: ResourceStore) {
+    super();
+    this.store = store;
+  }
+
+  public async canHandle(input: Operation): Promise<void> {
+    if (input.method !== 'PATCH') {
+      throw new UnsupportedHttpError('This handler only supports PATCH operations.');
+    }
+  }
+
+  public async handle(input: Operation): Promise<ResponseDescription> {
+    await this.store.modifyResource(input.target, input.body as Patch);
+    return { identifier: input.target };
+  }
+}
diff --git a/src/storage/PatchingStore.ts b/src/storage/PatchingStore.ts
new file mode 100644
index 000000000..ee03e3725
--- /dev/null
+++ b/src/storage/PatchingStore.ts
@@ -0,0 +1,46 @@
+import { Conditions } from './Conditions';
+import { Patch } from '../ldp/http/Patch';
+import { PatchHandler } from './patch/PatchHandler';
+import { Representation } from '../ldp/representation/Representation';
+import { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
+import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
+import { ResourceStore } from './ResourceStore';
+
+/**
+ * {@link ResourceStore} using decorator pattern for the `modifyResource` function.
+ * If the original store supports the {@link Patch}, behaviour will be identical,
+ * otherwise one of the {@link PatchHandler}s supporting the given Patch will be called instead.
+ */
+export class PatchingStore implements ResourceStore {
+  private readonly source: ResourceStore;
+  private readonly patcher: PatchHandler;
+
+  public constructor(source: ResourceStore, patcher: PatchHandler) {
+    this.source = source;
+    this.patcher = patcher;
+  }
+
+  public async addResource(container: ResourceIdentifier, representation: Representation, conditions?: Conditions): Promise<ResourceIdentifier> {
+    return this.source.addResource(container, representation, conditions);
+  }
+
+  public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<void> {
+    return this.source.deleteResource(identifier, conditions);
+  }
+
+  public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences, conditions?: Conditions): Promise<Representation> {
+    return this.source.getRepresentation(identifier, preferences, conditions);
+  }
+
+  public async setRepresentation(identifier: ResourceIdentifier, representation: Representation, conditions?: Conditions): Promise<void> {
+    return this.source.setRepresentation(identifier, representation, conditions);
+  }
+
+  public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise<void> {
+    try {
+      return await this.source.modifyResource(identifier, patch, conditions);
+    } catch (error) {
+      return this.patcher.handleSafe({ identifier, patch });
+    }
+  }
+}
diff --git a/src/storage/patch/PatchHandler.ts b/src/storage/patch/PatchHandler.ts
new file mode 100644
index 000000000..9ef49b6a3
--- /dev/null
+++ b/src/storage/patch/PatchHandler.ts
@@ -0,0 +1,5 @@
+import { AsyncHandler } from '../../util/AsyncHandler';
+import { Patch } from '../../ldp/http/Patch';
+import { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
+
+export abstract class PatchHandler extends AsyncHandler<{identifier: ResourceIdentifier; patch: Patch}> {}
diff --git a/src/storage/patch/SimpleSparqlUpdatePatchHandler.ts b/src/storage/patch/SimpleSparqlUpdatePatchHandler.ts
new file mode 100644
index 000000000..b2111daaa
--- /dev/null
+++ b/src/storage/patch/SimpleSparqlUpdatePatchHandler.ts
@@ -0,0 +1,82 @@
+import { Algebra } from 'sparqlalgebrajs';
+import { BaseQuad } from 'rdf-js';
+import { defaultGraph } from '@rdfjs/data-model';
+import { PatchHandler } from './PatchHandler';
+import { Readable } from 'stream';
+import { Representation } from '../../ldp/representation/Representation';
+import { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
+import { ResourceLocker } from '../ResourceLocker';
+import { ResourceStore } from '../ResourceStore';
+import { someTerms } from 'rdf-terms';
+import { SparqlUpdatePatch } from '../../ldp/http/SparqlUpdatePatch';
+import { Store } from 'n3';
+import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
+
+/**
+ * PatchHandler that supports specific types of SPARQL updates.
+ * Currently all DELETE/INSERT types are supported that have empty where bodies and no variables.
+ */
+export class SimpleSparqlUpdatePatchHandler extends PatchHandler {
+  private readonly source: ResourceStore;
+  private readonly locker: ResourceLocker;
+
+  public constructor(source: ResourceStore, locker: ResourceLocker) {
+    super();
+    this.source = source;
+    this.locker = locker;
+  }
+
+  public async canHandle(input: {identifier: ResourceIdentifier; patch: SparqlUpdatePatch}): Promise<void> {
+    if (input.patch.dataType !== 'sparql-algebra' || !input.patch.algebra) {
+      throw new UnsupportedHttpError('Only SPARQL update patch requests are supported.');
+    }
+  }
+
+  public async handle(input: {identifier: ResourceIdentifier; patch: SparqlUpdatePatch}): Promise<void> {
+    const op = input.patch.algebra;
+    if (!this.isDeleteInsert(op)) {
+      throw new UnsupportedHttpError('Only DELETE/INSERT SPARQL update operations are supported.');
+    }
+
+    const def = defaultGraph();
+    const deletes = op.delete || [];
+    const inserts = op.insert || [];
+
+    if (!deletes.every((pattern): boolean => pattern.graph.equals(def))) {
+      throw new UnsupportedHttpError('GRAPH statements are not supported.');
+    }
+    if (!inserts.every((pattern): boolean => pattern.graph.equals(def))) {
+      throw new UnsupportedHttpError('GRAPH statements are not supported.');
+    }
+    if (op.where || deletes.some((pattern): boolean => someTerms(pattern, (term): boolean => term.termType === 'Variable'))) {
+      throw new UnsupportedHttpError('WHERE statements are not supported.');
+    }
+
+    const lock = await this.locker.acquire(input.identifier);
+    const quads = await this.source.getRepresentation(input.identifier, { type: [{ value: 'internal/quads', weight: 1 }]});
+    const store = new Store<BaseQuad>();
+    const importEmitter = store.import(quads.data);
+    await new Promise((resolve, reject): void => {
+      importEmitter.on('end', resolve);
+      importEmitter.on('error', reject);
+    });
+    store.removeQuads(deletes);
+    store.addQuads(inserts);
+    const representation: Representation = {
+      data: store.match() as Readable,
+      dataType: 'quad',
+      metadata: {
+        raw: [],
+        profiles: [],
+        contentType: 'internal/quads',
+      },
+    };
+    await this.source.setRepresentation(input.identifier, representation);
+
+    await lock.release();
+  }
+
+  private isDeleteInsert(op: Algebra.Operation): op is Algebra.DeleteInsert {
+    return op.type === Algebra.types.DELETE_INSERT;
+  }
+}
diff --git a/src/util/Util.ts b/src/util/Util.ts
index 0083ed726..f8b461013 100644
--- a/src/util/Util.ts
+++ b/src/util/Util.ts
@@ -1,3 +1,6 @@
+import arrayifyStream from 'arrayify-stream';
+import { Readable } from 'stream';
+
 /**
  * Makes sure the input path has exactly 1 slash at the end.
  * Multiple slashes will get merged into one.
@@ -8,3 +11,5 @@
  * @returns The potentially changed path.
  */
 export const ensureTrailingSlash = (path: string): string => path.replace(/\/*$/u, '/');
+
+export const readableToString = async(stream: Readable): Promise<string> => (await arrayifyStream(stream)).join('');


Test patch:
diff --git a/test/integration/AuthenticatedLdpHandler.test.ts b/test/integration/AuthenticatedLdpHandler.test.ts
index 233bcf5d6..4ddb7037c 100644
--- a/test/integration/AuthenticatedLdpHandler.test.ts
+++ b/test/integration/AuthenticatedLdpHandler.test.ts
@@ -1,29 +1,59 @@
 import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
 import { AuthenticatedLdpHandler } from '../../src/ldp/AuthenticatedLdpHandler';
+import { BodyParser } from '../../src/ldp/http/BodyParser';
 import { CompositeAsyncHandler } from '../../src/util/CompositeAsyncHandler';
 import { EventEmitter } from 'events';
+import { HttpHandler } from '../../src/server/HttpHandler';
 import { HttpRequest } from '../../src/server/HttpRequest';
+import { IncomingHttpHeaders } from 'http';
 import { Operation } from '../../src/ldp/operations/Operation';
+import { Parser } from 'n3';
+import { PatchingStore } from '../../src/storage/PatchingStore';
+import { Representation } from '../../src/ldp/representation/Representation';
 import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription';
 import { SimpleAuthorizer } from '../../src/authorization/SimpleAuthorizer';
 import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser';
 import { SimpleCredentialsExtractor } from '../../src/authentication/SimpleCredentialsExtractor';
 import { SimpleDeleteOperationHandler } from '../../src/ldp/operations/SimpleDeleteOperationHandler';
 import { SimpleGetOperationHandler } from '../../src/ldp/operations/SimpleGetOperationHandler';
+import { SimplePatchOperationHandler } from '../../src/ldp/operations/SimplePatchOperationHandler';
 import { SimplePermissionsExtractor } from '../../src/ldp/permissions/SimplePermissionsExtractor';
 import { SimplePostOperationHandler } from '../../src/ldp/operations/SimplePostOperationHandler';
 import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser';
 import { SimpleResourceStore } from '../../src/storage/SimpleResourceStore';
 import { SimpleResponseWriter } from '../../src/ldp/http/SimpleResponseWriter';
+import { SimpleSparqlUpdateBodyParser } from '../../src/ldp/http/SimpleSparqlUpdateBodyParser';
+import { SimpleSparqlUpdatePatchHandler } from '../../src/storage/patch/SimpleSparqlUpdatePatchHandler';
 import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor';
+import { SingleThreadedResourceLocker } from '../../src/storage/SingleThreadedResourceLocker';
 import streamifyArray from 'streamify-array';
 import { createResponse, MockResponse } from 'node-mocks-http';
+import { namedNode, quad } from '@rdfjs/data-model';
 import * as url from 'url';
 
-describe('An AuthenticatedLdpHandler with instantiated handlers', (): void => {
-  let handler: AuthenticatedLdpHandler;
+const call = async(handler: HttpHandler, requestUrl: url.URL, method: string, headers: IncomingHttpHeaders, data: string[]): Promise<MockResponse<any>> => {
+  const request = streamifyArray(data) as HttpRequest;
+  request.url = requestUrl.pathname;
+  request.method = method;
+  request.headers = headers;
+  request.headers.host = requestUrl.host;
+  const response: MockResponse<any> = createResponse({ eventEmitter: EventEmitter });
+
+  const endPromise = new Promise((resolve): void => {
+    response.on('end', (): void => {
+      expect(response._isEndCalled()).toBeTruthy();
+      resolve();
+    });
+  });
+
+  await handler.handleSafe({ request, response });
+  await endPromise;
+
+  return response;
+};
 
-  beforeEach(async(): Promise<void> => {
+describe('An AuthenticatedLdpHandler', (): void => {
+  describe('with simple handlers', (): void => {
     const requestParser = new SimpleRequestParser({
       targetExtractor: new SimpleTargetExtractor(),
       preferenceParser: new AcceptPreferenceParser(),
@@ -43,7 +73,7 @@ describe('An AuthenticatedLdpHandler with instantiated handlers', (): void => {
 
     const responseWriter = new SimpleResponseWriter();
 
-    handler = new AuthenticatedLdpHandler({
+    const handler = new AuthenticatedLdpHandler({
       requestParser,
       credentialsExtractor,
       permissionsExtractor,
@@ -51,101 +81,125 @@ describe('An AuthenticatedLdpHandler with instantiated handlers', (): void => {
       operationHandler,
       responseWriter,
     });
-  });
 
-  it('can add, read and delete data based on incoming requests.', async(): Promise<void> => {
-    // POST
-    let requestUrl = new url.URL('http://test.com/');
-    let request = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
-    request.url = requestUrl.pathname;
-    request.method = 'POST';
-    request.headers = {
-      'content-type': 'text/turtle',
-      host: requestUrl.host,
-    };
-    let response: MockResponse<any> = createResponse({ eventEmitter: EventEmitter });
-
-    let id;
-    let endPromise = new Promise((resolve): void => {
-      response.on('end', (): void => {
-        expect(response._isEndCalled()).toBeTruthy();
-        expect(response.statusCode).toBe(200);
-        expect(response._getData()).toHaveLength(0);
-        id = response._getHeaders().location;
-        expect(id).toContain(url.format(requestUrl));
-        resolve();
-      });
+    it('can add, read and delete data based on incoming requests.', async(): Promise<void> => {
+      // POST
+      let requestUrl = new url.URL('http://test.com/');
+      let response: MockResponse<any> = await call(
+        handler,
+        requestUrl,
+        'POST',
+        { 'content-type': 'text/turtle' },
+        [ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
+      );
+      expect(response.statusCode).toBe(200);
+      expect(response._getData()).toHaveLength(0);
+      const id = response._getHeaders().location;
+      expect(id).toContain(url.format(requestUrl));
+
+      // GET
+      requestUrl = new url.URL(id);
+      response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []);
+      expect(response.statusCode).toBe(200);
+      expect(response._getData()).toContain('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
+      expect(response._getHeaders().location).toBe(id);
+
+      // DELETE
+      response = await call(handler, requestUrl, 'DELETE', {}, []);
+      expect(response.statusCode).toBe(200);
+      expect(response._getData()).toHaveLength(0);
+      expect(response._getHeaders().location).toBe(url.format(requestUrl));
+
+      // GET
+      response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []);
+      expect(response.statusCode).toBe(404);
+      expect(response._getData()).toContain('NotFoundHttpError');
     });
+  });
 
-    await handler.handleSafe({ request, response });
-    await endPromise;
-
-    // GET
-    requestUrl = new url.URL(id);
-    request = {} as HttpRequest;
-    request.url = requestUrl.pathname;
-    request.method = 'GET';
-    request.headers = {
-      accept: 'text/turtle',
-      host: requestUrl.host,
-    };
-    response = createResponse({ eventEmitter: EventEmitter });
-
-    endPromise = new Promise((resolve): void => {
-      response.on('end', (): void => {
-        expect(response._isEndCalled()).toBeTruthy();
-        expect(response.statusCode).toBe(200);
-        expect(response._getData()).toContain('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
-        expect(response._getHeaders().location).toBe(url.format(requestUrl));
-        resolve();
-      });
+  describe('with simple PATCH handlers', (): void => {
+    const bodyParser: BodyParser = new CompositeAsyncHandler<HttpRequest, Representation>([
+      new SimpleBodyParser(),
+      new SimpleSparqlUpdateBodyParser(),
+    ]);
+    const requestParser = new SimpleRequestParser({
+      targetExtractor: new SimpleTargetExtractor(),
+      preferenceParser: new AcceptPreferenceParser(),
+      bodyParser,
     });
 
-    await handler.handleSafe({ request, response });
-    await endPromise;
-
-    // DELETE
-    request = {} as HttpRequest;
-    request.url = requestUrl.pathname;
-    request.method = 'DELETE';
-    request.headers = {
-      host: requestUrl.host,
-    };
-    response = createResponse({ eventEmitter: EventEmitter });
-
-    endPromise = new Promise((resolve): void => {
-      response.on('end', (): void => {
-        expect(response._isEndCalled()).toBeTruthy();
-        expect(response.statusCode).toBe(200);
-        expect(response._getData()).toHaveLength(0);
-        expect(response._getHeaders().location).toBe(url.format(requestUrl));
-        resolve();
-      });
-    });
+    const credentialsExtractor = new SimpleCredentialsExtractor();
+    const permissionsExtractor = new SimplePermissionsExtractor();
+    const authorizer = new SimpleAuthorizer();
 
-    await handler.handleSafe({ request, response });
-    await endPromise;
-
-    // GET
-    request = {} as HttpRequest;
-    request.url = requestUrl.pathname;
-    request.method = 'GET';
-    request.headers = {
-      accept: 'text/turtle',
-      host: requestUrl.host,
-    };
-    response = createResponse({ eventEmitter: EventEmitter });
-
-    endPromise = new Promise((resolve): void => {
-      response.on('end', (): void => {
-        expect(response._isEndCalled()).toBeTruthy();
-        expect(response.statusCode).toBe(404);
-        expect(response._getData()).toContain('NotFoundHttpError');
-        resolve();
-      });
+    const store = new SimpleResourceStore('http://test.com/');
+    const locker = new SingleThreadedResourceLocker();
+    const patcher = new SimpleSparqlUpdatePatchHandler(store, locker);
+    const patchingStore = new PatchingStore(store, patcher);
+
+    const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
+      new SimpleGetOperationHandler(patchingStore),
+      new SimplePostOperationHandler(patchingStore),
+      new SimpleDeleteOperationHandler(patchingStore),
+      new SimplePatchOperationHandler(patchingStore),
+    ]);
+
+    const responseWriter = new SimpleResponseWriter();
+
+    const handler = new AuthenticatedLdpHandler({
+      requestParser,
+      credentialsExtractor,
+      permissionsExtractor,
+      authorizer,
+      operationHandler,
+      responseWriter,
     });
 
-    await handler.handleSafe({ request, response });
-    await endPromise;
+    it('can handle simple SPARQL updates.', async(): Promise<void> => {
+      // POST
+      let requestUrl = new url.URL('http://test.com/');
+      let response: MockResponse<any> = await call(
+        handler,
+        requestUrl,
+        'POST',
+        { 'content-type': 'text/turtle' },
+        [ '<http://test.com/s1> <http://test.com/p1> <http://test.com/o1>.',
+          '<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.' ],
+      );
+      expect(response.statusCode).toBe(200);
+      expect(response._getData()).toHaveLength(0);
+      const id = response._getHeaders().location;
+      expect(id).toContain(url.format(requestUrl));
+
+      // PATCH
+      requestUrl = new url.URL(id);
+      response = await call(
+        handler,
+        requestUrl,
+        'PATCH',
+        { 'content-type': 'application/sparql-update' },
+        [ 'DELETE { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1> }',
+          'INSERT {<http://test.com/s3> <http://test.com/p3> <http://test.com/o3>}',
+          'WHERE {}' ],
+      );
+      expect(response.statusCode).toBe(200);
+      expect(response._getData()).toHaveLength(0);
+      expect(response._getHeaders().location).toBe(id);
+
+      // GET
+      requestUrl = new url.URL(id);
+      response = await call(handler, requestUrl, 'GET', { accept: 'text/turtle' }, []);
+      expect(response.statusCode).toBe(200);
+      expect(response._getData()).toContain('<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.');
+      expect(response._getHeaders().location).toBe(id);
+      const parser = new Parser();
+      const triples = parser.parse(response._getData());
+      expect(triples).toBeRdfIsomorphic(
+        [
+          quad(namedNode('http://test.com/s2'), namedNode('http://test.com/p2'), namedNode('http://test.com/o2')),
+          quad(namedNode('http://test.com/s3'), namedNode('http://test.com/p3'), namedNode('http://test.com/o3')),
+        ],
+      );
+    });
   });
 });
diff --git a/test/unit/ldp/http/SimpleSparqlUpdateBodyParser.test.ts b/test/unit/ldp/http/SimpleSparqlUpdateBodyParser.test.ts
new file mode 100644
index 000000000..267de1410
--- /dev/null
+++ b/test/unit/ldp/http/SimpleSparqlUpdateBodyParser.test.ts
@@ -0,0 +1,41 @@
+import { Algebra } from 'sparqlalgebrajs';
+import { HttpRequest } from '../../../../src/server/HttpRequest';
+import { SimpleSparqlUpdateBodyParser } from '../../../../src/ldp/http/SimpleSparqlUpdateBodyParser';
+import streamifyArray from 'streamify-array';
+import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
+import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
+import { namedNode, quad } from '@rdfjs/data-model';
+
+describe('A SimpleSparqlUpdateBodyParser', (): void => {
+  const bodyParser = new SimpleSparqlUpdateBodyParser();
+
+  it('only accepts application/sparql-update content.', async(): Promise<void> => {
+    await expect(bodyParser.canHandle({ headers: {}} as HttpRequest)).rejects.toThrow(UnsupportedMediaTypeHttpError);
+    await expect(bodyParser.canHandle({ headers: { 'content-type': 'text/plain' }} as HttpRequest)).rejects.toThrow(UnsupportedMediaTypeHttpError);
+    await expect(bodyParser.canHandle({ headers: { 'content-type': 'application/sparql-update' }} as HttpRequest)).resolves.toBeUndefined();
+  });
+
+  it('errors when handling invalid SPARQL updates.', async(): Promise<void> => {
+    await expect(bodyParser.handle(streamifyArray([ 'VERY INVALID UPDATE' ]) as HttpRequest)).rejects.toThrow(UnsupportedHttpError);
+  });
+
+  it('converts SPARQL updates to algebra.', async(): Promise<void> => {
+    const result = await bodyParser.handle(streamifyArray(
+      [ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o>}' ],
+    ) as HttpRequest);
+    expect(result.algebra.type).toBe(Algebra.types.DELETE_INSERT);
+    expect((result.algebra as Algebra.DeleteInsert).delete).toBeRdfIsomorphic([ quad(
+      namedNode('http://test.com/s'),
+      namedNode('http://test.com/p'),
+      namedNode('http://test.com/o'),
+    ) ]);
+    expect(result.dataType).toBe('sparql-algebra');
+    expect(result.raw).toBe('DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o>}');
+    expect(result.metadata).toEqual({
+      raw: [],
+      profiles: [],
+      contentType: 'application/sparql-update',
+    });
+    expect((): any => result.data).toThrow('Body already parsed');
+  });
+});
diff --git a/test/unit/ldp/operations/SimplePatchOperationHandler.test.ts b/test/unit/ldp/operations/SimplePatchOperationHandler.test.ts
new file mode 100644
index 000000000..1b51b1c96
--- /dev/null
+++ b/test/unit/ldp/operations/SimplePatchOperationHandler.test.ts
@@ -0,0 +1,24 @@
+import { Operation } from '../../../../src/ldp/operations/Operation';
+import { ResourceStore } from '../../../../src/storage/ResourceStore';
+import { SimplePatchOperationHandler } from '../../../../src/ldp/operations/SimplePatchOperationHandler';
+import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
+
+describe('A SimplePatchOperationHandler', (): void => {
+  const store = {} as unknown as ResourceStore;
+  const handler = new SimplePatchOperationHandler(store);
+  beforeEach(async(): Promise<void> => {
+    store.modifyResource = jest.fn(async(): Promise<void> => undefined);
+  });
+
+  it('only supports GET operations.', async(): Promise<void> => {
+    await expect(handler.canHandle({ method: 'PATCH' } as Operation)).resolves.toBeUndefined();
+    await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(UnsupportedHttpError);
+  });
+
+  it('deletes the resource from the store and returns its identifier.', async(): Promise<void> => {
+    await expect(handler.handle({ target: { path: 'url' }, body: { dataType: 'patch' }} as Operation))
+      .resolves.toEqual({ identifier: { path: 'url' }});
+    expect(store.modifyResource).toHaveBeenCalledTimes(1);
+    expect(store.modifyResource).toHaveBeenLastCalledWith({ path: 'url' }, { dataType: 'patch' });
+  });
+});
diff --git a/test/unit/storage/PatchingStore.test.ts b/test/unit/storage/PatchingStore.test.ts
new file mode 100644
index 000000000..594476f5b
--- /dev/null
+++ b/test/unit/storage/PatchingStore.test.ts
@@ -0,0 +1,67 @@
+import { PatchHandler } from '../../../src/storage/patch/PatchHandler';
+import { PatchingStore } from '../../../src/storage/PatchingStore';
+import { ResourceStore } from '../../../src/storage/ResourceStore';
+
+describe('A PatchingStore', (): void => {
+  let store: PatchingStore;
+  let source: ResourceStore;
+  let patcher: PatchHandler;
+  let handleSafeFn: jest.Mock<Promise<void>, []>;
+
+  beforeEach(async(): Promise<void> => {
+    source = {
+      getRepresentation: jest.fn(async(): Promise<any> => 'get'),
+      addResource: jest.fn(async(): Promise<any> => 'add'),
+      setRepresentation: jest.fn(async(): Promise<any> => 'set'),
+      deleteResource: jest.fn(async(): Promise<any> => 'delete'),
+      modifyResource: jest.fn(async(): Promise<any> => 'modify'),
+    };
+
+    handleSafeFn = jest.fn(async(): Promise<any> => 'patcher');
+    patcher = { handleSafe: handleSafeFn } as unknown as PatchHandler;
+
+    store = new PatchingStore(source, patcher);
+  });
+
+  it('calls getRepresentation directly from the source.', async(): Promise<void> => {
+    await expect(store.getRepresentation({ path: 'getPath' }, null)).resolves.toBe('get');
+    expect(source.getRepresentation).toHaveBeenCalledTimes(1);
+    expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: 'getPath' }, null, undefined);
+  });
+
+  it('calls addResource directly from the source.', async(): Promise<void> => {
+    await expect(store.addResource({ path: 'addPath' }, null)).resolves.toBe('add');
+    expect(source.addResource).toHaveBeenCalledTimes(1);
+    expect(source.addResource).toHaveBeenLastCalledWith({ path: 'addPath' }, null, undefined);
+  });
+
+  it('calls setRepresentation directly from the source.', async(): Promise<void> => {
+    await expect(store.setRepresentation({ path: 'setPath' }, null)).resolves.toBe('set');
+    expect(source.setRepresentation).toHaveBeenCalledTimes(1);
+    expect(source.setRepresentation).toHaveBeenLastCalledWith({ path: 'setPath' }, null, undefined);
+  });
+
+  it('calls deleteResource directly from the source.', async(): Promise<void> => {
+    await expect(store.deleteResource({ path: 'deletePath' }, null)).resolves.toBe('delete');
+    expect(source.deleteResource).toHaveBeenCalledTimes(1);
+    expect(source.deleteResource).toHaveBeenLastCalledWith({ path: 'deletePath' }, null);
+  });
+
+  it('calls modifyResource directly from the source if available.', async(): Promise<void> => {
+    await expect(store.modifyResource({ path: 'modifyPath' }, null)).resolves.toBe('modify');
+    expect(source.modifyResource).toHaveBeenCalledTimes(1);
+    expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'modifyPath' }, null, undefined);
+  });
+
+  it('calls its patcher if modifyResource failed.', async(): Promise<void> => {
+    source.modifyResource = jest.fn(async(): Promise<any> => {
+      throw new Error('dummy');
+    });
+    await expect(store.modifyResource({ path: 'modifyPath' }, null)).resolves.toBe('patcher');
+    expect(source.modifyResource).toHaveBeenCalledTimes(1);
+    expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'modifyPath' }, null, undefined);
+    await expect((source.modifyResource as jest.Mock).mock.results[0].value).rejects.toThrow('dummy');
+    expect(handleSafeFn).toHaveBeenCalledTimes(1);
+    expect(handleSafeFn).toHaveBeenLastCalledWith({ identifier: { path: 'modifyPath' }, patch: null });
+  });
+});
diff --git a/test/unit/storage/patch/SimpleSparqlUpdatePatchHandler.test.ts b/test/unit/storage/patch/SimpleSparqlUpdatePatchHandler.test.ts
new file mode 100644
index 000000000..4a83bc6f3
--- /dev/null
+++ b/test/unit/storage/patch/SimpleSparqlUpdatePatchHandler.test.ts
@@ -0,0 +1,187 @@
+import arrayifyStream from 'arrayify-stream';
+import { Lock } from '../../../../src/storage/Lock';
+import { Quad } from 'rdf-js';
+import { ResourceLocker } from '../../../../src/storage/ResourceLocker';
+import { ResourceStore } from '../../../../src/storage/ResourceStore';
+import { SimpleSparqlUpdatePatchHandler } from '../../../../src/storage/patch/SimpleSparqlUpdatePatchHandler';
+import { SparqlUpdatePatch } from '../../../../src/ldp/http/SparqlUpdatePatch';
+import streamifyArray from 'streamify-array';
+import { translate } from 'sparqlalgebrajs';
+import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
+import { namedNode, quad } from '@rdfjs/data-model';
+
+describe('A SimpleSparqlUpdatePatchHandler', (): void => {
+  let handler: SimpleSparqlUpdatePatchHandler;
+  let locker: ResourceLocker;
+  let lock: Lock;
+  let release: () => Promise<void>;
+  let source: ResourceStore;
+  let startQuads: Quad[];
+  let order: string[];
+
+  beforeEach(async(): Promise<void> => {
+    order = [];
+
+    startQuads = [ quad(
+      namedNode('http://test.com/startS1'),
+      namedNode('http://test.com/startP1'),
+      namedNode('http://test.com/startO1'),
+    ), quad(
+      namedNode('http://test.com/startS2'),
+      namedNode('http://test.com/startP2'),
+      namedNode('http://test.com/startO2'),
+    ) ];
+
+    source = {
+      getRepresentation: jest.fn(async(): Promise<any> => {
+        order.push('getRepresentation');
+        return {
+          dataType: 'quads',
+          data: streamifyArray([ ...startQuads ]),
+          metadata: null,
+        };
+      }),
+      addResource: null,
+      setRepresentation: jest.fn(async(): Promise<any> => {
+        order.push('setRepresentation');
+      }),
+      deleteResource: null,
+      modifyResource: jest.fn(async(): Promise<any> => {
+        throw new Error('noModify');
+      }),
+    };
+
+    release = jest.fn(async(): Promise<any> => order.push('release'));
+    locker = {
+      acquire: jest.fn(async(): Promise<any> => {
+        order.push('acquire');
+        lock = { release };
+        return lock;
+      }),
+    };
+
+    handler = new SimpleSparqlUpdatePatchHandler(source, locker);
+  });
+
+  const basicChecks = async(quads: Quad[]): Promise<void> => {
+    expect(source.getRepresentation).toHaveBeenCalledTimes(1);
+    expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: 'path' }, { type: [{ value: 'internal/quads', weight: 1 }]});
+    expect(source.setRepresentation).toHaveBeenCalledTimes(1);
+    expect(order).toEqual([ 'acquire', 'getRepresentation', 'setRepresentation', 'release' ]);
+    const setParams = (source.setRepresentation as jest.Mock).mock.calls[0];
+    expect(setParams[0]).toEqual({ path: 'path' });
+    expect(setParams[1]).toEqual(expect.objectContaining({
+      dataType: 'quad',
+      metadata: { raw: [], profiles: [], contentType: 'internal/quads' },
+    }));
+    await expect(arrayifyStream(setParams[1].data)).resolves.toBeRdfIsomorphic(quads);
+  };
+
+  it('only accepts SPARQL updates.', async(): Promise<void> => {
+    const input = { identifier: { path: 'path' }, patch: { dataType: 'sparql-algebra', algebra: {}} as SparqlUpdatePatch };
+    await expect(handler.canHandle(input)).resolves.toBeUndefined();
+    input.patch.dataType = 'notAlgebra';
+    await expect(handler.canHandle(input)).rejects.toThrow(UnsupportedHttpError);
+  });
+
+  it('handles INSERT DATA updates.', async(): Promise<void> => {
+    await handler.handle({ identifier: { path: 'path' },
+      patch: { algebra: translate(
+        'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. ' +
+        '<http://test.com/s2> <http://test.com/p2> <http://test.com/o2> }',
+        { quads: true },
+      ) } as SparqlUpdatePatch });
+    await basicChecks(startQuads.concat(
+      [ quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')),
+        quad(namedNode('http://test.com/s2'), namedNode('http://test.com/p2'), namedNode('http://test.com/o2')) ],
+    ));
+  });
+
+  it('handles DELETE DATA updates.', async(): Promise<void> => {
+    await handler.handle({ identifier: { path: 'path' },
+      patch: { algebra: translate(
+        'DELETE DATA { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }',
+        { quads: true },
+      ) } as SparqlUpdatePatch });
+    await basicChecks(
+      [ quad(namedNode('http://test.com/startS2'), namedNode('http://test.com/startP2'), namedNode('http://test.com/startO2')) ],
+    );
+  });
+
+  it('handles DELETE WHERE updates with no variables.', async(): Promise<void> => {
+    await handler.handle({ identifier: { path: 'path' },
+      patch: { algebra: translate(
+        'DELETE WHERE { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }',
+        { quads: true },
+      ) } as SparqlUpdatePatch });
+    await basicChecks(
+      [ quad(namedNode('http://test.com/startS2'), namedNode('http://test.com/startP2'), namedNode('http://test.com/startO2')) ],
+    );
+  });
+
+  it('handles DELETE/INSERT updates with empty WHERE.', async(): Promise<void> => {
+    await handler.handle({ identifier: { path: 'path' },
+      patch: { algebra: translate(
+        'DELETE { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }\n' +
+        'INSERT { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }\n' +
+        'WHERE {}',
+        { quads: true },
+      ) } as SparqlUpdatePatch });
+    await basicChecks(
+      [ quad(namedNode('http://test.com/startS2'), namedNode('http://test.com/startP2'), namedNode('http://test.com/startO2')),
+        quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')) ],
+    );
+  });
+
+  it('rejects GRAPH inserts.', async(): Promise<void> => {
+    const handle = handler.handle({ identifier: { path: 'path' },
+      patch: { algebra: translate(
+        'INSERT DATA { GRAPH <http://test.com/graph> { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> } }',
+        { quads: true },
+      ) } as SparqlUpdatePatch });
+    await expect(handle).rejects.toThrow('GRAPH statements are not supported.');
+    expect(order).toEqual([]);
+  });
+
+  it('rejects GRAPH deletes.', async(): Promise<void> => {
+    const handle = handler.handle({ identifier: { path: 'path' },
+      patch: { algebra: translate(
+        'DELETE DATA { GRAPH <http://test.com/graph> { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> } }',
+        { quads: true },
+      ) } as SparqlUpdatePatch });
+    await expect(handle).rejects.toThrow('GRAPH statements are not supported.');
+    expect(order).toEqual([]);
+  });
+
+  it('rejects DELETE/INSERT updates with a non-empty WHERE.', async(): Promise<void> => {
+    const handle = handler.handle({ identifier: { path: 'path' },
+      patch: { algebra: translate(
+        'DELETE { <http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }\n' +
+        'INSERT { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }\n' +
+        'WHERE { ?s ?p ?o }',
+        { quads: true },
+      ) } as SparqlUpdatePatch });
+    await expect(handle).rejects.toThrow('WHERE statements are not supported.');
+    expect(order).toEqual([]);
+  });
+
+  it('rejects DELETE WHERE updates with variables.', async(): Promise<void> => {
+    const handle = handler.handle({ identifier: { path: 'path' },
+      patch: { algebra: translate(
+        'DELETE WHERE { ?v <http://test.com/startP1> <http://test.com/startO1> }',
+        { quads: true },
+      ) } as SparqlUpdatePatch });
+    await expect(handle).rejects.toThrow('WHERE statements are not supported.');
+    expect(order).toEqual([]);
+  });
+
+  it('rejects non-DELETE/INSERT updates.', async(): Promise<void> => {
+    const handle = handler.handle({ identifier: { path: 'path' },
+      patch: { algebra: translate(
+        'MOVE DEFAULT TO GRAPH <http://test.com/newGraph>',
+        { quads: true },
+      ) } as SparqlUpdatePatch });
+    await expect(handle).rejects.toThrow('Only DELETE/INSERT SPARQL update operations are supported.');
+    expect(order).toEqual([]);
+  });
+});