Skip to content

Commit

Permalink
fix: separate timestamp insertion from entity data
Browse files Browse the repository at this point in the history
  • Loading branch information
adlerfaulkner committed Oct 11, 2023
1 parent 43aa641 commit 66bd10a
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 69 deletions.
4 changes: 2 additions & 2 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
@@ -1,6 +1,6 @@
{
"name": "@comake/skl-js-engine",
"version": "0.15.2",
"version": "0.15.3",
"description": "Standard Knowledge Language Javascript Engine",
"keywords": [
"skl",
Expand Down
131 changes: 71 additions & 60 deletions src/storage/sparql/SparqlUpdateBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ import { VariableGenerator } from './VariableGenerator';
export interface EntityUpdateQueries {
clear: ClearDropOperation[];
insertions: GraphQuads[];
timestampInsertions: GraphQuads[];
}

export interface EntityUpdateTriples {
entityTriples: Triple[];
timestampTriples: Triple[];
}

export interface SparqlUpdateBuilderArgs {
Expand All @@ -62,22 +68,21 @@ export class SparqlUpdateBuilder {

public buildUpdate(entityOrEntities: Entity | Entity[]): Update {
const entities = ensureArray(entityOrEntities);
const { clear, insertions } = this.entitiesToGraphDeletionsAndInsertions(entities);
let insertUpdate: InsertDeleteOperation;
if (this.setTimestamps) {
insertUpdate = {
const { clear, insertions, timestampInsertions } = this.entitiesToGraphDeletionsAndInsertions(entities);
const insertUpdate: InsertDeleteOperation = {
updateType: 'insert',
insert: insertions,
};
const updates = [ ...clear, insertUpdate ];
if (timestampInsertions.length > 0) {
updates.push({
updateType: 'insertdelete',
delete: [],
insert: insertions,
insert: timestampInsertions,
where: [ bindNow ],
};
} else {
insertUpdate = {
updateType: 'insert',
insert: insertions,
};
});
}
return createSparqlUpdate([ ...clear, insertUpdate ]);
return createSparqlUpdate(updates);
}

public buildDelete(entityOrEntities: Entity | Entity[]): Update {
Expand All @@ -94,37 +99,45 @@ export class SparqlUpdateBuilder {
ids: string[],
attributes: Partial<Entity>,
): InsertDeleteOperation[] {
return ids.map((id): InsertDeleteOperation => {
return ids.flatMap((id): InsertDeleteOperation[] => {
const subject = DataFactory.namedNode(id);
const deletionTriples = this.partialEntityToDeletionTriples(attributes, subject);
const insertionTriples = this.partialEntityToTriples(subject, attributes);
const whereTriples = [ ...deletionTriples ];
const whereAdditions: Pattern[] = [];
const updates: InsertDeleteOperation[] = [
{
updateType: 'insertdelete',
delete: [ createSparqlGraphQuads(subject, deletionTriples) ],
insert: [ createSparqlGraphQuads(subject, insertionTriples) ],
where: deletionTriples.map((triple): Pattern =>
createSparqlOptional([
createSparqlBasicGraphPattern([ triple ]),
])),
using: {
default: [ subject ],
},
} as InsertDeleteOperation,
];
if (this.setTimestamps) {
const modifiedVariable = DataFactory.variable(this.variableGenerator.getNext());
const modifiedDeletionTriple = { subject, predicate: modified, object: modifiedVariable };
const modifiedInsertionTriple = { subject, predicate: modified, object: now };
deletionTriples.push(modifiedDeletionTriple);
insertionTriples.push(modifiedInsertionTriple);
whereTriples.push(modifiedDeletionTriple);
whereAdditions.push(bindNow);
}
const update = {
updateType: 'insertdelete',
delete: [ createSparqlGraphQuads(subject, deletionTriples) ],
insert: [ createSparqlGraphQuads(subject, insertionTriples) ],
where: [
...whereTriples.map((triple): Pattern =>
updates.push({
updateType: 'insertdelete',
delete: [ createSparqlGraphQuads(subject, [ modifiedDeletionTriple ]) ],
insert: [ createSparqlGraphQuads(subject, [ modifiedInsertionTriple ]) ],
where: [
createSparqlOptional([
createSparqlBasicGraphPattern([ triple ]),
])),
...whereAdditions,
],
using: {
default: [ subject ],
},
} as InsertDeleteOperation;
return update;
createSparqlBasicGraphPattern([ modifiedDeletionTriple ]),
]),
bindNow,
],
using: {
default: [ subject ],
},
} as InsertDeleteOperation);
}

return updates;
});
}

Expand All @@ -134,11 +147,14 @@ export class SparqlUpdateBuilder {
return entities
.reduce((obj: EntityUpdateQueries, entity): EntityUpdateQueries => {
const entityGraphName = DataFactory.namedNode(entity['@id']);
const insertionTriples = this.entityToTriples(entity, entityGraphName);
const { entityTriples, timestampTriples } = this.entityToTriples(entity, entityGraphName);
obj.clear.push(createSparqlClearUpdate(entityGraphName));
obj.insertions.push(createSparqlGraphQuads(entityGraphName, insertionTriples));
obj.insertions.push(createSparqlGraphQuads(entityGraphName, entityTriples));
if (timestampTriples.length > 0) {
obj.timestampInsertions.push(createSparqlGraphQuads(entityGraphName, timestampTriples));
}
return obj;
}, { clear: [], insertions: []});
}, { clear: [], insertions: [], timestampInsertions: []});
}

private entitiesToGraphDropUpdates(
Expand Down Expand Up @@ -187,43 +203,37 @@ export class SparqlUpdateBuilder {
return entityTriples;
}

private entityToTriples(entity: NodeObject, subject: BlankNode | NamedNode): Triple[] {
private entityToTriples(entity: NodeObject, subject: BlankNode | NamedNode): EntityUpdateTriples {
const entityTriples = Object.entries(entity).reduce((triples: Triple[], [ key, value ]): Triple[] => {
const values = ensureArray(value);
if (key !== '@id') {
let predicateTriples: Triple[];
if (key === '@type') {
predicateTriples = this.buildTriplesWithSubjectPredicateAndIriValue(
const predicateTriples = this.buildTriplesWithSubjectPredicateAndIriValue(
subject,
rdfTypeNamedNode,
values as string[],
);
} else {
predicateTriples = this.buildTriplesForSubjectPredicateAndValues(subject, key, values);
return [ ...triples, ...predicateTriples ];
}
if (!(this.setTimestamps && key === DCTERMS.modified)) {
const predicateTriples = this.buildTriplesForSubjectPredicateAndValues(subject, key, values);
return [ ...triples, ...predicateTriples ];
}
return [ ...triples, ...predicateTriples ];
}
return triples;
}, []);

const timestampTriples = [];
if (this.setTimestamps && subject.termType === 'NamedNode') {
if (!entity[DCTERMS.created]) {
entityTriples.push({ subject, predicate: created, object: now });
}
if (entity[DCTERMS.modified]) {
for (const triple of entityTriples) {
if (triple.subject.equals(subject) &&
'termType' in triple.predicate &&
triple.predicate.equals(modified)
) {
triple.object = now;
}
}
} else {
entityTriples.push({ subject, predicate: modified, object: now });
if (!(DCTERMS.created in entity)) {
timestampTriples.push({ subject, predicate: created, object: now });
}
timestampTriples.push({ subject, predicate: modified, object: now });
}
return entityTriples;
return {
entityTriples,
timestampTriples,
};
}

private buildTriplesForSubjectPredicateAndValues(
Expand Down Expand Up @@ -313,9 +323,10 @@ export class SparqlUpdateBuilder {

private buildTriplesForBlankNode(subject: BlankNode | NamedNode, predicate: NamedNode, value: NodeObject): Triple[] {
const blankNode = DataFactory.blankNode(this.variableGenerator.getNext());
const { entityTriples } = this.entityToTriples(value, blankNode);
return [
{ subject, predicate, object: blankNode },
...this.entityToTriples(value, blankNode),
...entityTriples,
];
}
}
47 changes: 46 additions & 1 deletion test/unit/storage/sparql/SparqlQueryAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import SparqlClient from 'sparql-http-client';
import { InverseRelation } from '../../../../src/storage/operator/InverseRelation';
import { SparqlQueryAdapter } from '../../../../src/storage/sparql/SparqlQueryAdapter';
import { rdfTypeNamedNode } from '../../../../src/util/SparqlUtil';
import { SKL, XSD } from '../../../../src/util/Vocabularies';
import { DCTERMS, SKL, XSD } from '../../../../src/util/Vocabularies';
import { streamFrom } from '../../../util/Util';

const endpointUrl = 'https://example.com/sparql';
Expand Down Expand Up @@ -758,6 +758,28 @@ describe('a SparqlQueryAdapter', (): void => {
'INSERT DATA { GRAPH <https://example.com/data/1> { <https://example.com/data/1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://standardknowledge.com/ontologies/core/File>. } }',
]);
});

it('saves a single schema with setTimestamps on.', async(): Promise<void> => {
adapter = new SparqlQueryAdapter({ type: 'sparql', endpointUrl, setTimestamps: true });
const entity = {
'@id': 'https://example.com/data/1',
'@type': 'https://standardknowledge.com/ontologies/core/File',
};
await expect(adapter.save(entity)).resolves.toEqual(entity);
expect(update).toHaveBeenCalledTimes(1);
expect(update.mock.calls[0][0].split('\n')).toEqual([
'CLEAR SILENT GRAPH <https://example.com/data/1>;',
'INSERT DATA { GRAPH <https://example.com/data/1> { <https://example.com/data/1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://standardknowledge.com/ontologies/core/File>. } };',
'INSERT {',
' GRAPH <https://example.com/data/1> {',
` <https://example.com/data/1> <${DCTERMS.created}> ?now.`,
` <https://example.com/data/1> <${DCTERMS.modified}> ?now.`,
' }',
'}',
'WHERE { BIND(NOW() AS ?now) }',
]);
});

it('saves multiple schema.', async(): Promise<void> => {
const entities = [
{
Expand Down Expand Up @@ -796,6 +818,29 @@ describe('a SparqlQueryAdapter', (): void => {
`WHERE { OPTIONAL { <https://example.com/data/1> <${SKL.sourceId}> ?c1. } }`,
]);
});

it('updates a schema with setTimestamps on.', async(): Promise<void> => {
adapter = new SparqlQueryAdapter({ type: 'sparql', endpointUrl, setTimestamps: true });
await expect(adapter.update(
'https://example.com/data/1',
{ [SKL.sourceId]: 'abc123' },
)).resolves.toBeUndefined();
expect(update).toHaveBeenCalledTimes(1);
expect(update.mock.calls[0][0].split('\n')).toEqual([
`DELETE { GRAPH <https://example.com/data/1> { <https://example.com/data/1> <${SKL.sourceId}> ?c1. } }`,
`INSERT { GRAPH <https://example.com/data/1> { <https://example.com/data/1> <${SKL.sourceId}> "abc123". } }`,
'USING <https://example.com/data/1>',
`WHERE { OPTIONAL { <https://example.com/data/1> <${SKL.sourceId}> ?c1. } };`,
`DELETE { GRAPH <https://example.com/data/1> { <https://example.com/data/1> <${DCTERMS.modified}> ?c2. } }`,
`INSERT { GRAPH <https://example.com/data/1> { <https://example.com/data/1> <${DCTERMS.modified}> ?now. } }`,
'USING <https://example.com/data/1>',
`WHERE {`,
` OPTIONAL { <https://example.com/data/1> <${DCTERMS.modified}> ?c2. }`,
' BIND(NOW() AS ?now)',
'}',
]);
});

it('updates multiple schemas by attribute.', async(): Promise<void> => {
await expect(adapter.update(
[ 'https://example.com/data/1', 'https://example.com/data/2' ],
Expand Down
Loading

0 comments on commit 66bd10a

Please sign in to comment.