Skip to content

Commit

Permalink
m-ld/m-ld-spec#35, #48: Inline filters and binds
Browse files Browse the repository at this point in the history
  • Loading branch information
gsvarovsky committed Apr 18, 2023
1 parent 3a8dc99 commit 84b02ed
Show file tree
Hide file tree
Showing 11 changed files with 463 additions and 193 deletions.
5 changes: 3 additions & 2 deletions src/engine/MeldEncoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { SubjectGraph } from './SubjectGraph';
import { SubjectQuads } from './SubjectQuads';
// TODO: Switch to fflate. Node.js zlib uses Pako in the browser
import { gunzipSync, gzipSync } from 'zlib';
import { baseVocab, domainBase } from './dataset/index';
import { baseVocab, domainBase } from './dataset';
import { MeldError } from '../api';
import { JrqlMode } from './jrql-util';

const COMPRESS_THRESHOLD_BYTES = 1024;

Expand Down Expand Up @@ -125,7 +126,7 @@ export class MeldEncoder {
};

triplesFromJson = (json: object): Triple[] =>
[...new SubjectQuads('graph', this.ctx, this.rdf).quads(<any>json)];
new SubjectQuads(this.rdf, JrqlMode.graph, this.ctx).quads(<any>json);

triplesFromBuffer = (encoded: Buffer, encoding: BufferEncoding[]): Triple[] =>
this.triplesFromJson(MeldEncoder.jsonFromBuffer(encoded, encoding));
Expand Down
154 changes: 119 additions & 35 deletions src/engine/SubjectQuads.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,78 @@
import { any, anyName, blank } from '../api';
import {
Atom,
Constraint,
InlineConstraint,
isInlineConstraint,
isPropertyObject,
isReference,
isSet,
isValueObject,
isVocabReference,
Reference,
Subject,
SubjectPropertyObject
SubjectPropertyObject,
VariableExpression
} from '../jrql-support';
import { JsonldContext, mapValue } from './jsonld';
import { Quad, Quad_Object, Quad_Subject, RdfFactory } from './quads';
import { asQueryVar, Quad, Quad_Object, Quad_Subject, RdfFactory } from './quads';
import { JRQL, RDF } from '../ns';
import { JrqlMode, ListIndex, listItems, toIndexDataUrl } from './jrql-util';
import { isArray, lazy } from './util';
import { isArray, lazy, mapObject } from './util';
import { array } from '../util';

export class SubjectQuads {
const NO_VARS: ReadonlySet<string> = new Set<string>();

export interface InlineConstraints {
filters: ReadonlyArray<Constraint>,
binds: ReadonlyArray<VariableExpression>
}

export namespace InlineConstraints {
export const NONE: InlineConstraints = { filters: [], binds: [] };
}

export class SubjectQuads implements InlineConstraints {
/** Populated with inline filters found, if mode is 'match' */
private readonly _filters?: Constraint[];
/** Populated with inline bindings found, if mode is 'load' */
private readonly _binds?: VariableExpression[];

/**
* @param rdf
* @param mode
* @param ctx
* @param _vars Populated with variable names found (sans '?')
*/
constructor(
readonly rdf: RdfFactory,
readonly mode: JrqlMode,
readonly ctx: JsonldContext,
readonly rdf: RdfFactory,
readonly vars?: Set<string>
) {}
private readonly _vars?: Set<string>
) {
if (mode === JrqlMode.match)
this._filters = [];
if (mode === JrqlMode.load)
this._binds = [];
}

get vars(): ReadonlySet<string> {
return this._vars ?? NO_VARS;
}

get filters(): ReadonlyArray<Constraint> {
return this._filters ?? [];
}

*quads(
get binds(): ReadonlyArray<VariableExpression> {
return this._binds ?? [];
}

quads(subjects: Subject | Subject[]) {
return [...this.process(subjects)];
}

private *process(
object: SubjectPropertyObject,
outer: Quad_Subject | null = null,
property: string | null = null
Expand All @@ -34,17 +81,23 @@ export class SubjectQuads {
for (let value of array(object))
if (isArray(value))
// Nested array is flattened
yield *this.quads(value, outer, property);
yield *this.process(value, outer, property);
else if (isSet(value))
// @set is elided
yield *this.quads(value['@set'], outer, property);
else if (typeof value === 'object' && !isValueObject(value) && !isVocabReference(value))
yield *this.process(value['@set'], outer, property);
else if (typeof value === 'object' &&
!isValueObject(value) &&
!isVocabReference(value) &&
!isInlineConstraint(value))
// TODO: @json type, nested @context object
yield *this.subjectQuads(value, outer, property);
else if (outer != null && property != null)
// This is an atom, so yield one quad
yield this.rdf.quad(outer, this.predicate(property),
this.objectTerm(value, property));
yield this.rdf.quad(
outer,
this.predicate(property),
this.objectTerm(value, property)
);
// TODO: What if the property expands to a keyword in the context?
else
throw new Error(`Cannot yield quad from top-level value: ${value}`);
Expand All @@ -62,7 +115,7 @@ export class SubjectQuads {
if (outer != null && property != null)
// Yield the outer quad referencing this subject
yield this.rdf.quad(outer, this.predicate(property), sid);
else if (this.mode === 'match' && isReference(subject))
else if (this.mode === JrqlMode.match && isReference(subject))
// References at top level => implicit wildcard p-o
yield this.rdf.quad(sid, this.genVar(), this.genVar());

Expand All @@ -72,7 +125,7 @@ export class SubjectQuads {
if (property === '@list')
yield *this.listQuads(sid, value);
else
yield *this.quads(value, sid, property);
yield *this.process(value, sid, property);
}

private subjectId(subject: Subject) {
Expand All @@ -81,9 +134,9 @@ export class SubjectQuads {
return this.rdf.blankNode(subject['@id']);
else
return this.expandNode(subject['@id']);
else if (this.mode === 'match')
else if (this.mode === JrqlMode.match)
return this.genVar();
else if (this.mode === 'load' && this.rdf.skolem != null)
else if (this.mode === JrqlMode.load && this.rdf.skolem != null)
return this.rdf.skolem();
else
return this.rdf.blankNode(blank());
Expand All @@ -109,7 +162,7 @@ export class SubjectQuads {
// Generate the slot id variable if not available
if (!('@id' in slot))
slot['@id'] = JRQL.subVar(index, 'slotId');
} else if (this.mode !== 'match') {
} else if (this.mode !== JrqlMode.match) {
// Inserting at a numeric index
indexKey = toIndexDataUrl(index);
} else {
Expand All @@ -119,33 +172,33 @@ export class SubjectQuads {
indexKey = slotVarName ? JRQL.subVar(slotVarName, 'listKey') : any();
}
// Slot index is never asserted, only entailed
if (this.mode === 'match')
if (this.mode === JrqlMode.match)
// Sub-index should never exist for matching
slot['@index'] = typeof index == 'string' ? `?${index}` : index[0];
// This will yield the index key as a property, as well as the slot
yield *this.quads(slot, lid, indexKey);
yield *this.process(slot, lid, indexKey);
}

/** @returns a mutable proto-slot object */
private asSlot(item: SubjectPropertyObject): Subject {
if (isArray(item))
// A nested list is a nested list (not flattened or a set)
return { '@item': { '@list': item } };
if (typeof item == 'object' && ('@item' in item || this.mode === 'graph'))
if (typeof item == 'object' && ('@item' in item || this.mode === JrqlMode.graph))
// The item is already a slot (with an @item key)
return { ...item };
else
return { '@item': item };
}

private matchVar = (term: string) => {
if (this.mode !== 'graph') {
if (this.mode !== JrqlMode.graph) {
const varName = JRQL.matchVar(term);
if (varName != null) {
if (!varName)
// Allow anonymous variables as '?'
return this.genVar();
this.vars?.add(varName);
this._vars?.add(varName);
return this.rdf.variable(varName);
}
}
Expand All @@ -171,24 +224,55 @@ export class SubjectQuads {

private genVarName() {
const varName = anyName();
this.vars?.add(varName);
this._vars?.add(varName);
return varName;
}

private genVar() {
return this.rdf.variable(this.genVarName());
}

objectTerm(value: Atom, property?: string): Quad_Object {
return mapValue<Quad_Object>(property ?? null, value, (value, type, language) => {
if (type === '@id' || type === '@vocab')
return this.rdf.namedNode(value);
else if (language)
return this.rdf.literal(value, language);
else if (type !== '@none')
return this.rdf.literal(value, this.rdf.namedNode(type));
else
return this.rdf.literal(value);
}, { ctx: this.ctx, interceptRaw: this.matchVar });
objectTerm(value: Atom | InlineConstraint, property?: string): Quad_Object {
if (this.mode !== JrqlMode.graph && isInlineConstraint(value)) {
let { variable, constraint } = this.inlineConstraintDetails(value);
// The variable is the 1st parameter of the resultant constraint expression.
constraint = mapObject(constraint, (operator, expression) => ({
[operator]: [asQueryVar(variable), ...array(expression)]
}));
if (this.mode === JrqlMode.match) {
// If we're matching, the variable is the object e.g. ?o > 1
this._filters?.push(constraint);
return variable;
} else /*if (this.mode === JrqlMode.load)*/ {
// If we're loading, the object is the return value of the expression e.g.
// ?o = ?x + 1, so we expect two variables
const returnVar = this.genVar();
this._binds?.push({ [asQueryVar(returnVar)]: constraint });
return returnVar;
}
} else {
return mapValue<Quad_Object>(property ?? null, value, (value, type, language) => {
if (type === '@id' || type === '@vocab')
return this.rdf.namedNode(value);
else if (language)
return this.rdf.literal(value, language);
else if (type !== '@none')
return this.rdf.literal(value, this.rdf.namedNode(type));
else
return this.rdf.literal(value);
}, { ctx: this.ctx, interceptRaw: this.matchVar });
}
}

private inlineConstraintDetails(inlineConstraint: InlineConstraint) {
if ('@value' in inlineConstraint) {
const variable = this.matchVar(inlineConstraint['@value']);
if (variable == null)
throw new Error(`Invalid variable for inline constraint: ${inlineConstraint}`);
const { '@value': _, ...constraint } = inlineConstraint;
return { variable, constraint };
} else {
return { variable: this.genVar(), constraint: inlineConstraint };
}
}
}
16 changes: 12 additions & 4 deletions src/engine/dataset/ConstructTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { Iri } from '@m-ld/jsonld';
import { anyName, blank, GraphSubject } from '../../api';
import {
Group, isList, isPropertyObject, isSet, isSubjectObject, Subject, SubjectProperty,
SubjectPropertyObject, Value, Variable
Group,
isList,
isPropertyObject,
isSet,
isSubjectObject,
Subject,
SubjectProperty,
SubjectPropertyObject,
Value,
Variable
} from '../../jrql-support';
import { matchVar } from '../../ns/json-rql';
import { array } from '../../util';
import { addPropertyObject, listItems } from '../jrql-util';
import { addPropertyObject, JrqlMode, listItems } from '../jrql-util';
import { JsonldContext } from '../jsonld';
import { jrqlProperty, jrqlValue } from '../SubjectGraph';
import { Binding } from '../../rdfjs-support';
Expand Down Expand Up @@ -61,7 +69,7 @@ class SubjectTemplate {
() => this.templateId = construct['@id']);
// Discover List variables
if (isList(construct))
for (let [index, item] of listItems(construct['@list'], 'match'))
for (let [index, item] of listItems(construct['@list'], JrqlMode.match))
withNamedVar(index, // Index may be a var name
(variable, name) => this.addProperty(['@list', variable], name, item),
index => this.addProperty(['@list', ...index], null, item));
Expand Down
Loading

0 comments on commit 84b02ed

Please sign in to comment.