Skip to content

Commit

Permalink
fix: Allow non-variable BGP boedies in SPARQL updates
Browse files Browse the repository at this point in the history
* fix: SPARQL algebra update

* fix: SPARQL algebra bgp only

* fix: No SPARQL variables and refactor tests
  • Loading branch information
matthieubosquet committed Feb 24, 2021
1 parent 1473632 commit 894d458
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 96 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
"rdf-parse": "^1.7.0",
"rdf-serialize": "^1.1.0",
"rdf-terms": "^1.5.1",
"sparqlalgebrajs": "^2.3.1",
"sparqlalgebrajs": "^2.5.0",
"sparqljs": "^3.1.2",
"streamify-array": "^1.0.1",
"uuid": "^8.3.0",
Expand Down
13 changes: 10 additions & 3 deletions src/storage/patch/SparqlUpdatePatchHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
return op.type === Algebra.types.COMPOSITE_UPDATE;
}

private isBasicGraphPatternWithoutVariables(op: Algebra.Operation): op is Algebra.Bgp {
if (op.type !== Algebra.types.BGP) {
return false;
}
return !(op.patterns as BaseQuad[]).some((pattern): boolean =>
someTerms(pattern, (term): boolean => term.termType === 'Variable'));
}

/**
* Checks if the input operation is of a supported type (DELETE/INSERT or composite of those)
*/
Expand All @@ -67,7 +75,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler {

/**
* Checks if the input DELETE/INSERT is supported.
* This means: no GRAPH statements, no DELETE WHERE.
* This means: no GRAPH statements, no DELETE WHERE containing terms of type Variable.
*/
private validateDeleteInsert(op: Algebra.DeleteInsert): void {
const def = defaultGraph();
Expand All @@ -81,8 +89,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
this.logger.warn('GRAPH statement in INSERT clause');
throw new NotImplementedHttpError('GRAPH statements are not supported');
}
if (op.where ?? deletes.some((pattern): boolean =>
someTerms(pattern, (term): boolean => term.termType === 'Variable'))) {
if (!(typeof op.where === 'undefined' || this.isBasicGraphPatternWithoutVariables(op.where))) {
this.logger.warn('WHERE statements are not supported');
throw new NotImplementedHttpError('WHERE statements are not supported');
}
Expand Down
125 changes: 37 additions & 88 deletions test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
let handler: SparqlUpdatePatchHandler;
let source: ResourceStore;
let startQuads: Quad[];
const fullfilledDataInsert = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 . }';

beforeEach(async(): Promise<void> => {
startQuads = [ quad(
Expand Down Expand Up @@ -56,6 +57,12 @@ describe('A SparqlUpdatePatchHandler', (): void => {
return true;
}

async function handle(query: string): Promise<void> {
const sparqlPrefix = 'prefix : <http://test.com/>\n';
return handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(sparqlPrefix.concat(query), { quads: true }) } as SparqlUpdatePatch });
}

it('only accepts SPARQL updates.', async(): Promise<void> => {
const input = { identifier: { path: 'path' },
patch: { algebra: {}} as SparqlUpdatePatch };
Expand All @@ -65,24 +72,15 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});

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 handle(fullfilledDataInsert);
expect(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')) ],
))).toBe(true);
});

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 handle('DELETE DATA { :startS1 :startP1 :startO1 }');
expect(await basicChecks(
[ quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
Expand All @@ -91,11 +89,8 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});

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 });
const query = 'DELETE WHERE { :startS1 :startP1 :startO1 }';
await handle(query);
expect(await basicChecks(
[ quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
Expand All @@ -104,13 +99,8 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});

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 });
const query = 'DELETE { :startS1 :startP1 :startO1 } INSERT { :s1 :p1 :o1 . } WHERE {}';
await handle(query);
expect(await basicChecks([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
Expand All @@ -122,14 +112,9 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});

it('handles composite INSERT/DELETE 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> };' +
'DELETE WHERE { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>.' +
'<http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> }',
{ quads: true },
) } as SparqlUpdatePatch });
const query = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 };' +
'DELETE WHERE { :s1 :p1 :o1 . :startS1 :startP1 :startO1 }';
await handle(query);
expect(await basicChecks([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
Expand All @@ -141,14 +126,9 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});

it('handles composite DELETE/INSERT updates.', async(): Promise<void> => {
await handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'DELETE DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>.' +
'<http://test.com/startS1> <http://test.com/startP1> <http://test.com/startO1> };' +
'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 });
const query = 'DELETE DATA { :s1 :p1 :o1 . :startS1 :startP1 :startO1 } ;' +
'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 }';
await handle(query);
expect(await basicChecks([
quad(namedNode('http://test.com/startS2'),
namedNode('http://test.com/startP2'),
Expand All @@ -163,80 +143,49 @@ describe('A SparqlUpdatePatchHandler', (): void => {
});

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');
const query = 'INSERT DATA { GRAPH :graph { :s1 :p1 :o1 } }';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});

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');
const query = 'DELETE DATA { GRAPH :graph { :s1 :p1 :o1 } }';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});

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');
const query = 'DELETE { :s1 :p1 :o1 } INSERT { :s1 :p1 :o1 } WHERE { ?s ?p ?o }';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});

it('rejects INSERT WHERE updates with a UNION.', async(): Promise<void> => {
const query = 'INSERT { :s1 :p1 :o1 . } WHERE { { :s1 :p1 :o1 } UNION { :s1 :p1 :o2 } }';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});

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');
const query = 'DELETE WHERE { ?v :startP1 :startO1 }';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});

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');
const query = 'MOVE DEFAULT TO GRAPH :newGraph';
await expect(handle(query)).rejects.toThrow(NotImplementedHttpError);
});

it('throws the error returned by the store if there is one.', async(): Promise<void> => {
source.getRepresentation = jest.fn(async(): Promise<any> => {
throw new Error('error');
});

const input = { 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 expect(handler.handle(input)).rejects.toThrow('error');
await expect(handle(fullfilledDataInsert)).rejects.toThrow('error');
});

it('creates a new resource if it does not exist yet.', async(): Promise<void> => {
// There is no initial data
startQuads = [];
source.getRepresentation = jest.fn((): any => {
throw new NotFoundHttpError();
});

await handler.handle({ identifier: { path: 'path' },
patch: { algebra: translate(
'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }',
{ quads: true },
) } as SparqlUpdatePatch });
const query = 'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }';
await handle(query);
expect(await basicChecks(startQuads.concat(
[ quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')) ],
))).toBe(true);
Expand Down

0 comments on commit 894d458

Please sign in to comment.