Skip to content

Commit

Permalink
Added support for filtering results (#81)
Browse files Browse the repository at this point in the history
Resolves #80.
  • Loading branch information
karelklima authored Dec 19, 2023
1 parent 11ad9b0 commit cbf2956
Show file tree
Hide file tree
Showing 12 changed files with 649 additions and 13 deletions.
1 change: 1 addition & 0 deletions docs/table-of-contents.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"v2": {
"title": "Version 2 (not yet released)",
"pages": [
["filtering", "Filtering"],
["pagination", "Pagination"],
["working-with-arrays", "Working with arrays"],
["inverse-properties", "Inverse properties"]
Expand Down
88 changes: 88 additions & 0 deletions docs/v2/filtering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Filtering

LDkit comes with powerful search and filtering capabilities, enabling users to
narrow data results and explore large datasets. The feature integrates
seamlessly with the existing [Lens](../components/lens) `find` method, allowing
for controlled data retrieval.

LDkit allows various search and filtering operations like `$equals`, `$not`,
`$contains`, `$strStarts`, `$strEnds`, `$gt`, `$lt`, `$gte`, `$lte`, `$regex`,
`$langMatches`, and `$filter`. Each is illustrated below with examples.

### Simple example

```ts
import { createLens } from "ldkit";
import { schema, xsd } from "ldkit/namespaces";

// Create a schema
const PersonSchema = {
"@type": schema.Person,
name: schema.name,
birthDate: {
"@id": schema.birthDate,
"@type": xsd.date,
},
} as const;

// Create a lens instance
const Persons = createLens(PersonSchema);

await Persons.find({
where: {
name: "Ada Lovelace",
},
}); // Returns list of all persons named "Ada Lovelace"
```

### Comparison operators

```typescript
await Persons.find({
where: {
name: {
$equals: "Ada Lovelace", // FILTER (?value = "Ada Lovelace")
$not: "Alan Turing", // FILTER (?value != "Alan Turing")
},
birthDate: {
$lt: new Date("01-01-1900"), // FILTER (?value < "01-01-1900"@xsd:date)
$lte: new Date("01-01-1900"), // FILTER (?value <= "01-01-1900"@xsd:date)
$gt: new Date("01-01-1900"), // FILTER (?value > "01-01-1900"@xsd:date)
$gte: new Date("01-01-1900"), // FILTER (?value >= "01-01-1900"@xsd:date)
},
},
});
```

### String functions

```typescript
await Persons.find({
where: {
name: {
$contains: "Ada", // FILTER CONTAINS(?value, "Ada")
$strStarts: "Ada", // FILTER STRSTARTS(?value, "Ada")
$strEnds: "Lovelace", // FILTER STRENDS(?value, "Lovelace")
$langMatches: "fr", // FILTER LANGMATCHES(LANG(?value), "fr")
$regex: "^A(.*)e$", // FILTER REGEX(?value, "^A(.*)e$")
},
},
});
```

### Custom filtering

On top of the above, it is possible to specify a custom filter function using
the SPARQL filtering syntax. There is a special placeholder `?value` used for
the value of the property to be filtered. This placeholder is replaced by actual
variable name during runtime.

```typescript
await Persons.find({
where: {
name: {
$filter: "STRLEN(?value) > 10", // FILTER (STRLEN(?value) > 10)
},
},
});
```
11 changes: 9 additions & 2 deletions library/engine/query_engine_proxy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { type Context, IQueryEngine, quadsToGraph, type RDF } from "../rdf.ts";
import {
type Context,
IQueryEngine,
N3,
quadsToGraph,
type RDF,
} from "../rdf.ts";
import { resolveContext, resolveEngine } from "../global.ts";
import { type AsyncIterator } from "../asynciterator.ts";

Expand Down Expand Up @@ -29,7 +35,8 @@ export class QueryEngineProxy {
this.context,
) as unknown as AsyncIterator<RDF.Quad>;
const quads = await (quadStream.toArray());
return quadsToGraph(quads);
const store = new N3.Store(quads);
return quadsToGraph(store);
}

queryVoid(query: string) {
Expand Down
14 changes: 11 additions & 3 deletions library/lens/lens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type SchemaInterface,
type SchemaInterfaceIdentity,
type SchemaPrototype,
type SchemaSearchInterface,
type SchemaUpdateInterface,
} from "../schema/mod.ts";
import { decode } from "../decoder.ts";
Expand Down Expand Up @@ -44,6 +45,7 @@ export class Lens<
S extends SchemaPrototype,
I = SchemaInterface<S>,
U = SchemaUpdateInterface<S>,
X = SchemaSearchInterface<S>,
> {
private readonly schema: Schema;
private readonly context: Context;
Expand Down Expand Up @@ -72,19 +74,25 @@ export class Lens<

async query(sparqlConstructQuery: string) {
const graph = await this.engine.queryGraph(sparqlConstructQuery);
console.log("GRAPH", graph);
return this.decode(graph);
}

async find(
options: { where?: string | RDF.Quad[]; take?: number; skip?: number } = {},
options: { where?: X | string | RDF.Quad[]; take?: number; skip?: number } =
{},
) {
const { where, take, skip } = {
take: 1000,
skip: 0,
...options,
};
const q = this.queryBuilder.getQuery(where, take, skip);
// TODO: console.log(q);
const isRegularQuery = typeof where === "string" ||
typeof where === "undefined" || Array.isArray(where);

const q = isRegularQuery
? this.queryBuilder.getQuery(where, take, skip)
: this.queryBuilder.getSearchQuery(where ?? {}, take, skip);
const graph = await this.engine.queryGraph(q);
return this.decode(graph);
}
Expand Down
64 changes: 57 additions & 7 deletions library/lens/query_builder.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { type Schema } from "../schema/mod.ts";
import { getSchemaProperties } from "../schema/mod.ts";
import {
getSchemaProperties,
type Property,
type Schema,
type SearchSchema,
} from "../schema/mod.ts";
import {
CONSTRUCT,
DELETE,
INSERT,
SELECT,
sparql as $,
type SparqlValue,
} from "../sparql/mod.ts";
import { type Context, DataFactory, type Iri, type RDF } from "../rdf.ts";
import ldkit from "../namespaces/ldkit.ts";
Expand All @@ -15,6 +20,7 @@ import { encode } from "../encoder.ts";

import { type Entity } from "./types.ts";
import { UpdateHelper } from "./update_helper.ts";
import { SearchHelper } from "./search_helper.ts";

enum Flags {
None = 0,
Expand Down Expand Up @@ -52,16 +58,33 @@ export class QueryBuilder {
return ([] as RDF.Quad[]).concat(...quadArrays);
}

private getShape(flags: Flags) {
private getShape(flags: Flags, searchSchema?: SearchSchema) {
const includeOptional = (flags & Flags.ExcludeOptional) === 0;
const wrapOptional = (flags & Flags.UnwrapOptional) === 0;
const omitRootTypes = (flags & Flags.IncludeTypes) === 0;
const ignoreInverse = (flags & Flags.IgnoreInverse) === Flags.IgnoreInverse;

const mainVar = "iri";
const conditions: (RDF.Quad | ReturnType<typeof $>)[] = [];
const conditions: SparqlValue[] = [];

const populateSearchConditions = (
property: Property,
varName: string,
search?: SearchSchema,
) => {
if (search === undefined) {
return;
}
const helper = new SearchHelper(property, varName, search);
helper.process();
conditions.push(helper.sparqlValues);
};

const populateConditionsRecursive = (s: Schema, varPrefix: string) => {
const populateConditionsRecursive = (
s: Schema,
varPrefix: string,
search?: SearchSchema,
) => {
const rdfType = s["@type"];
const properties = getSchemaProperties(s);

Expand All @@ -80,7 +103,8 @@ export class QueryBuilder {
Object.keys(properties).forEach((prop, index) => {
const property = properties[prop];
const isOptional = property["@optional"];
if (!includeOptional && isOptional) {
const propertySchema = search?.[prop] as SearchSchema | undefined;
if (!includeOptional && isOptional && propertySchema === undefined) {
return;
}
if (wrapOptional && isOptional) {
Expand All @@ -95,6 +119,11 @@ export class QueryBuilder {
this.df.variable!(`${varPrefix}_${index}`),
),
);
populateSearchConditions(
property,
`${varPrefix}_${index}`,
propertySchema,
);
} else {
conditions.push(
this.df.quad(
Expand All @@ -108,6 +137,7 @@ export class QueryBuilder {
populateConditionsRecursive(
property["@context"] as Schema,
`${varPrefix}_${index}`,
propertySchema,
);
}
if (wrapOptional && isOptional) {
Expand All @@ -116,7 +146,7 @@ export class QueryBuilder {
});
};

populateConditionsRecursive(this.schema, mainVar);
populateConditionsRecursive(this.schema, mainVar, searchSchema);
return conditions;
}

Expand Down Expand Up @@ -146,6 +176,26 @@ export class QueryBuilder {
return query;
}

getSearchQuery(where: SearchSchema, limit: number, offset: number) {
const selectSubQuery = SELECT.DISTINCT`
${this.df.variable!("iri")}
`.WHERE`
${this.getShape(Flags.ExcludeOptional | Flags.IncludeTypes, where)}
`.LIMIT(limit).OFFSET(offset).build();

const query = CONSTRUCT`
${this.getResourceSignature()}
${this.getShape(Flags.UnwrapOptional | Flags.IgnoreInverse)}
`.WHERE`
{
${selectSubQuery}
}
${this.getShape(Flags.None, where)}
`.build();

return query;
}

getByIrisQuery(iris: Iri[]) {
const query = CONSTRUCT`
${this.getResourceSignature()}
Expand Down
Loading

0 comments on commit cbf2956

Please sign in to comment.