diff --git a/.changeset/long-files-look.md b/.changeset/long-files-look.md new file mode 100644 index 0000000..d5e9e6d --- /dev/null +++ b/.changeset/long-files-look.md @@ -0,0 +1,5 @@ +--- +"@rdfjs/types": patch +--- + +Make queryable metadata types configurable diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000..1d976b1 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,13 @@ +{ + "mode": "exit", + "tag": "next", + "initialVersions": { + "@rdfjs/types": "1.0.1" + }, + "changesets": [ + "friendly-lies-suffer", + "long-files-look", + "seven-shrimps-build", + "tough-guests-flash" + ] +} diff --git a/.changeset/tough-guests-flash.md b/.changeset/tough-guests-flash.md new file mode 100644 index 0000000..ac39a32 --- /dev/null +++ b/.changeset/tough-guests-flash.md @@ -0,0 +1,5 @@ +--- +"@rdfjs/types": minor +--- + +Add queryable interfaces diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cebc897..1e53d3b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - feature/query jobs: release: diff --git a/CHANGELOG.md b/CHANGELOG.md index 779f93c..8100a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # @rdfjs/types +## 1.1.0-next.1 + +### Patch Changes + +- Make queryable metadata types configurable + +## 1.1.0-next.0 + +### Minor Changes + +- 95f1e31: Dataset: Use correct type of `dataset` in methods with callbacks +- bc7163e: Add queryable interfaces + +### Patch Changes + +- 8164183: Documentation Fix: Update reference of Quad to BaseQuad in the definition of Term in order to align with the type declaration. + ## 1.0.1 ### Patch Changes diff --git a/index.d.ts b/index.d.ts index 9e2c7fb..b7a4b96 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,3 +3,4 @@ export * from './data-model'; export * from './stream'; export * from './dataset'; +export * from './query'; diff --git a/package.json b/package.json index edeb377..2c7bed2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rdfjs/types", - "version": "1.0.1", + "version": "1.1.0-next.1", "license": "MIT", "types": "index.d.ts", "author": { diff --git a/query.d.ts b/query.d.ts new file mode 100644 index 0000000..9645aa3 --- /dev/null +++ b/query.d.ts @@ -0,0 +1,7 @@ +/* Query Interfaces */ +/* https://rdf.js.org/query-spec/ */ + +export * from './query/common'; +export * from './query/queryable'; + + diff --git a/query/common.d.ts b/query/common.d.ts new file mode 100644 index 0000000..f2a63cf --- /dev/null +++ b/query/common.d.ts @@ -0,0 +1,287 @@ +/* Query Interfaces - Common */ +/* https://rdf.js.org/query-spec/ */ + +import { EventEmitter } from "events"; +import * as RDF from '../data-model'; + +/** + * Helper union type for quad term names. + */ +export type QuadTermName = 'subject' | 'predicate' | 'object' | 'graph'; + +// TODO: merge this with Stream upon the next major change +/** + * Custom typings for the RDF/JS ResultStream interface as the current + * typings restrict the generic param Q to extensions of "BaseQuad", + * meaning it cannot be used for Bindings. + */ +export interface ResultStream extends EventEmitter { + read(): Q | null; +} + +/** + * QueryOperationCost represents the cost of a given query operation. + */ +export interface QueryOperationCost { + /** + * An estimation of how many iterations over items are executed. + * This is used to determine the CPU cost. + */ + iterations: number; + /** + * An estimation of how many items are stored in memory. + * This is used to determine the memory cost. + */ + persistedItems: number; + /** + * An estimation of how many items block the stream. + * This is used to determine the time the stream is not progressing anymore. + */ + blockingItems: number; + /** + * An estimation of the time to request items from sources. + * This is used to determine the I/O cost. + */ + requestTime: number; + /** + * Custom properties + */ + [key: string]: any; +} + +/** + * QueryOperationOrder represents an ordering of the results of a given query operation. + * + * These objects can represent orderings of both quad and bindings streams, + * respectively identified by quad term names and variables. + */ +export interface QueryOperationOrder { + cost: QueryOperationCost; + terms: { term: T, direction: 'asc' | 'desc' }[]; +} + +/** + * QueryResultCardinality represents the number of results, which can either be an estimate or exact value. + */ +export interface QueryResultCardinality { + /** + * indicates the type of counting that was done, and MUST either be "estimate" or "exact". + */ + type: 'estimate' | 'exact'; + + /** + * Indicates an estimated of the number of results in the stream if type = "estimate", + * or the exact number of results in the stream if type = "exact". + */ + value: number; +} + +/** + * BaseMetadataQuery is helper interface that provides a metadata callback. + */ +interface BaseMetadataQuery { + /** + * Asynchronously return metadata of the current result. + */ + metadata>(opts?: M): Promise>; +} + +export type AllMetadataSupport = CardinalityMetadataSupport & OrderMetadataSupport & AvailableOrdersMetadataSupport; +export type CardinalityMetadataSupport = { cardinality: true }; +export type OrderMetadataSupport = { order: true }; +export type AvailableOrdersMetadataSupport = { availableOrders: true }; + +export type MetadataOpts = + (SupportedMetadataType extends CardinalityMetadataSupport ? CardinalityMetadataOpts : unknown) | + (SupportedMetadataType extends OrderMetadataSupport ? OrderMetadataOpts : unknown) | + (SupportedMetadataType extends AvailableOrdersMetadataSupport ? AvailableOrdersMetadataOpts : unknown); +export interface CardinalityMetadataOpts { cardinality: 'estimate' | 'exact'; } +export interface OrderMetadataOpts { order: true; } +export interface AvailableOrdersMetadataOpts { availableOrders: true; } + +export type ConditionalMetadataType = AdditionalMetadataType + & (M extends CardinalityMetadataOpts ? { cardinality: QueryResultCardinality } : Record) + & (M extends OrderMetadataOpts ? { order: QueryOperationOrder['terms'] } : Record) + & (M extends AvailableOrdersMetadataOpts ? { availableOrders: QueryOperationOrder[] } : Record); + +/** + * Options that can be passed when executing a query. + */ +export interface QueryExecuteOptions { + /** + * The required order for the result stream. + */ + order?: QueryOperationOrder; + + /** + * Custom properties + */ + [key: string]: any; +} + +/** + * Generic interface that defines query objects following the Future pattern. + */ +export interface BaseQuery { + /** + * Identifier for the type of result of tis query. + */ + resultType: string; + + /** + * Returns either a stream containing all the items that match the given query, + * a boolean or void depending on the semantics of the given query. + */ + execute(opts?: any): Promise | boolean | void>; +} + +/** + * Query object that returns bindings. + */ +export interface QueryBindings extends BaseQuery, BaseMetadataQuery { + resultType: 'bindings'; + execute(opts?: QueryExecuteOptions): Promise>; +} + +/** + * Query object that returns quads. + */ +export interface QueryQuads extends BaseQuery, BaseMetadataQuery { + resultType: 'quads'; + execute(opts?: QueryExecuteOptions): Promise>; +} + +/** + * Query object that returns a boolean. + */ +export interface QueryBoolean extends BaseQuery { + resultType: 'boolean'; + execute(): Promise; +} + +/** + * Query object that returns void. + */ +export interface QueryVoid extends BaseQuery { + resultType: 'void'; + execute(): Promise; +} + +/** + * Union type for the different query types. + */ +export type Query = QueryBindings | QueryBoolean | QueryQuads | QueryVoid; + +/** + * Bindings represents the mapping of variables to RDF values using an immutable Map-like representation. + * This means that methods such as `set` and `delete` do not modify this instance, + * but they return a new Bindings instance that contains the modification. + * + * Bindings instances are created using a BindingsFactory. + * + * The internal order of variable-value entries is undefined. + */ +export interface Bindings extends Iterable<[RDF.Variable, RDF.Term]> { + type: 'bindings'; + /** + * Check if a binding exist for the given variable. + * @param key A variable term or string. If it is a string, no `?` prefix must be given. + */ + has: (key: RDF.Variable | string) => boolean; + /** + * Obtain the binding value for the given variable. + * @param key A variable term or string. If it is a string, no `?` prefix must be given. + */ + get: (key: RDF.Variable | string) => RDF.Term | undefined; + /** + * Create a new Bindings object by adding the given variable and value mapping. + * + * If the variable already exists in the binding, then the existing mapping is overwritten. + * + * @param key The variable key term or string. If it is a string, no `?` prefix must be given. + * @param value The value. + */ + set: (key: RDF.Variable | string, value: RDF.Term) => Bindings; + /** + * Create a new Bindings object by removing the given variable. + * + * If the variable does not exist in the binding, a copy of the Bindings object is returned. + * + * @param key The variable key term or string. If it is a string, no `?` prefix must be given. + */ + delete: (key: RDF.Variable | string) => Bindings; + /** + * Obtain all variables for which mappings exist. + */ + keys: () => Iterable; + /** + * Obtain all values that are mapped to. + */ + values: () => Iterable; + /** + * Iterate over all variable-value pairs. + * @param fn A callback that is called for each variable-value pair + * with value as first argument, and variable as second argument. + */ + forEach: (fn: (value: RDF.Term, key: RDF.Variable) => any) => void; + /** + * The number of variable-value pairs. + */ + size: number; + /** + * Iterator over all variable-value pairs. + */ + [Symbol.iterator]: () => Iterator<[RDF.Variable, RDF.Term]>; + /** + * Check if all entries contained in this Bindings object are equal to all entries in the other Bindings object. + * @param other A Bindings object. + */ + equals(other: Bindings | null | undefined): boolean; + /** + * Create a new Bindings object by filtering entries using a callback. + * @param fn A callback that is applied on each entry. + * Returning true indicates that this entry must be contained in the resulting Bindings object. + */ + filter: (fn: (value: RDF.Term, key: RDF.Variable) => boolean) => Bindings; + /** + * Create a new Bindings object by mapping entries using a callback. + * @param fn A callback that is applied on each entry, in which the original value is replaced by the returned value. + */ + map: (fn: (value: RDF.Term, key: RDF.Variable) => RDF.Term) => Bindings; + /** + * Merge this bindings with another. + * + * If a merge conflict occurs (this and other have an equal variable with unequal value), + * then undefined is returned. + * + * @param other A Bindings object. + */ + merge: (other: Bindings) => Bindings | undefined; + /** + * Merge this bindings with another, where merge conflicts can be resolved using a callback function. + * @param merger A function that is invoked when a merge conflict occurs, + * for which the returned value is considered the merged value. + * @param other A Bindings object. + */ + mergeWith: ( + merger: (self: RDF.Term, other: RDF.Term, key: RDF.Variable) => RDF.Term, + other: Bindings, + ) => Bindings; +} + +/** + * BindingsFactory can create new instances of Bindings. + */ +export interface BindingsFactory { + /** + * Create a new Bindings object from the given variable-value entries. + * @param entries An array of entries, where each entry is a tuple containing a variable and a term. + */ + bindings: (entries?: [RDF.Variable, RDF.Term][]) => Bindings; + + /** + * Create a copy of the given bindings object using this factory. + * @param bindings A Bindings object. + */ + fromBindings: (bindings: Bindings) => Bindings; +} diff --git a/query/queryable.d.ts b/query/queryable.d.ts new file mode 100644 index 0000000..7a98c86 --- /dev/null +++ b/query/queryable.d.ts @@ -0,0 +1,160 @@ +/* Query Interfaces - Queryable */ +/* https://rdf.js.org/query-spec/ */ + +import * as RDF from '../data-model'; +import { Bindings, Query, ResultStream } from './common'; + +/** + * Context properties provide a way to pass additional bits information to the query engine when executing a query. + */ +export interface QueryContext { + /** + * The date that should be used by SPARQL operations such as NOW(). + */ + queryTimestamp?: Date; + /** + * Other options + */ + [key: string]: any; +} + +/** + * Context properties in the case the passed query is a string. + */ +export interface QueryStringContext extends QueryContext { + /** + * The format in which the query string is defined. + * Defaults to { language: 'sparql', version: '1.1' } + */ + queryFormat?: QueryFormat; + /** + * The baseIRI for parsing the query. + */ + baseIRI?: string; +} + +/** + * Context properties in the case the passed query is an algebra object. + */ +export type QueryAlgebraContext = QueryContext; + +/** + * Context properties for engines that can query upon dynamic sets of sources. + */ +export interface QuerySourceContext { + /** + * An array of data sources the query engine must use. + */ + sources: [SourceType, ...SourceType[]]; +} + +/** + * Represents a specific query format + */ +export interface QueryFormat { + /** + * The query language, e.g. 'sparql'. + */ + language: string; + /** + * The version of the query language, e.g. '1.1'. + */ + version: string; + /** + * An optional array of extensions on the query language. + * The representation of these extensions is undefined. + */ + extensions?: string[]; +} + +/** + * Generic query engine interfaces. + * It allow engines to return any type of result object for string queries. + * @param SupportedMetadataType The allowed metadata types. + * @param QueryStringContextType Type of the string-based query context. + */ +export interface StringQueryable< + SupportedMetadataType, + QueryStringContextType extends QueryStringContext = QueryStringContext, +> { + /** + * Initiate a given query provided as a string. + * + * This will produce a future to a query result, which has to be executed to obtain the query results. + * + * This can reject given an unsupported or invalid query. + * + * @see Query + */ + query(query: string, context?: QueryStringContextType): Promise>; +} + +/** + * Generic query engine interfaces. + * It allow engines to return any type of result object for Algebra queries. + * @param AlgebraType The supported algebra types. + * @param SupportedMetadataType The allowed metadata types. + * @param QueryStringContextType Type of the algebra-based query context. + */ + export interface AlgebraQueryable< + AlgebraType, + SupportedMetadataType, + QueryAlgebraContextType extends QueryAlgebraContext = QueryAlgebraContext, +> { + /** + * Initiate a given query provided as an Algebra object. + * + * This will produce a future to a query result, which has to be executed to obtain the query results. + * + * This can reject given an unsupported or invalid query. + * + * @see Query + */ + query(query: AlgebraType, context?: QueryAlgebraContextType): Promise>; +} + +/** + * SPARQL-constrained query interface for queries provided as strings. + * + * This interface guarantees that result objects are of the expected type as defined by the SPARQL spec. + */ +export type StringSparqlQueryable = unknown + & (SupportedResultType extends BindingsResultSupport ? { + queryBindings(query: string, context?: QueryStringContextType): Promise>; +} : unknown) + & (SupportedResultType extends BooleanResultSupport ? { + queryBoolean(query: string, context?: QueryStringContextType): Promise; +} : unknown) + & (SupportedResultType extends QuadsResultSupport ? { + queryQuads(query: string, context?: QueryStringContextType): Promise>; +} : unknown) + & (SupportedResultType extends VoidResultSupport ? { + queryVoid(query: string, context?: QueryStringContextType): Promise; +} : unknown) +; + +/** + * SPARQL-constrainted query interface for queries provided as Algebra objects. + * + * This interface guarantees that result objects are of the expected type as defined by the SPARQL spec. + */ + export type AlgebraSparqlQueryable = unknown + & (SupportedResultType extends BindingsResultSupport ? { + queryBindings(query: AlgebraType, context?: QueryAlgebraContextType): Promise>; +} : unknown) + & (SupportedResultType extends BooleanResultSupport ? { + queryBoolean(query: AlgebraType, context?: QueryAlgebraContextType): Promise; +} : unknown) + & (SupportedResultType extends QuadsResultSupport ? { + queryQuads(query: AlgebraType, context?: QueryAlgebraContextType): Promise>; +} : unknown) + & (SupportedResultType extends VoidResultSupport ? { + queryVoid(query: AlgebraType, context?: QueryAlgebraContextType): Promise; +} : unknown) +; + +export type SparqlResultSupport = BindingsResultSupport & VoidResultSupport & QuadsResultSupport & BooleanResultSupport; +export type BindingsResultSupport = { bindings: true }; +export type VoidResultSupport = { void: true }; +export type QuadsResultSupport = { quads: true }; +export type BooleanResultSupport = { boolean: true }; diff --git a/rdf-js-query-tests.ts b/rdf-js-query-tests.ts new file mode 100644 index 0000000..fc67df0 --- /dev/null +++ b/rdf-js-query-tests.ts @@ -0,0 +1,116 @@ +/* eslint-disable no-case-declarations */ +import { + DataFactory, + BindingsFactory, + Bindings, + Term, + StringQueryable, + AlgebraQueryable, + SparqlResultSupport, + MetadataOpts, + QueryStringContext, + QueryAlgebraContext, + AllMetadataSupport, + Query, + Variable, + ResultStream, + Quad, + StringSparqlQueryable, + AlgebraSparqlQueryable, + BindingsResultSupport, + QuadsResultSupport, +} from "."; + +function test_bindings() { + const df: DataFactory = {}; + const bf: BindingsFactory = {}; + + const b1: Bindings = bf.bindings([ + [ df.variable!('varA'), df.namedNode('ex:a'), ], + [ df.variable!('varB'), df.literal('B'), ], + ]); + + const valueA: Term | undefined = b1.get('varA'); + const valueB: Term | undefined = b1.get(df.variable!('varB')); + + const b2: Bindings = b1 + .set('varA', df.namedNode('ex:2')) + .delete('varB') + .set(df.variable!('varB'), df.literal('B2')); + + for (const [ key, value ] of b2) { + const keytype: 'Variable' = key.termType; + const valuetype: string = value.termType; + } + for (const key of b2.keys()) { + const type: 'Variable' = key.termType; + } +} + +async function test_stringqueryable() { + const engine: StringQueryable = {}; + + const query: Query = await engine.query('SELECT * WHERE { ... }'); + switch (query.resultType) { + case 'bindings': + const metadata = await query.metadata(); + const variables: Variable[] = metadata.variables; + const bindings: ResultStream = await query.execute(); + bindings.on('data', (bindings: Bindings) => console.log(bindings)); + break; + case 'quads': + const quads: ResultStream = await query.execute(); + break; + case 'boolean': + const bool: boolean = await query.execute(); + break; + case 'void': + const done: void = await query.execute(); + break; + } +} + +async function test_stringsparqlqueryable() { + const engine: StringSparqlQueryable = {}; + + const bindings: ResultStream = await engine.queryBindings('SELECT * WHERE { ... }'); + const quads: ResultStream = await engine.queryQuads('CONSTRUCT WHERE { ... }'); + const bool: boolean = await engine.queryBoolean('ASK WHERE { ... }'); + const done: void = await engine.queryVoid('INSERT WHERE { ... }'); +} + +async function test_algebrasparqlqueryable() { + interface AlgebraType { mock: 'algebra' } + const engine: AlgebraSparqlQueryable = {}; + + const bindings: ResultStream = await engine.queryBindings({ mock: 'algebra' }); + const quads: ResultStream = await engine.queryQuads({ mock: 'algebra' }); + const bool: boolean = await engine.queryBoolean({ mock: 'algebra' }); + const done: void = await engine.queryVoid({ mock: 'algebra' }); + + // @ts-ignore + await engine.queryBoolean('ASK WHERE { ... }'); // Query type doesn't match AlgebraType +} + +async function test_stringsparqlqueryable_partial() { + const engine: StringSparqlQueryable = {}; + + const bindings: ResultStream = await engine.queryBindings('SELECT * WHERE { ... }'); + const quads: ResultStream = await engine.queryQuads('CONSTRUCT WHERE { ... }'); + // @ts-ignore + const bool: boolean = await engine.queryBoolean('ASK WHERE { ... }'); // Unsupported + // @ts-ignore + const done: void = await engine.queryVoid('INSERT WHERE { ... }'); // Unsupported +} + +async function test_algebrasparqlqueryable_partial() { + interface AlgebraType { mock: 'algebra' } + const engine: AlgebraSparqlQueryable = {}; + + const bindings: ResultStream = await engine.queryBindings({ mock: 'algebra' }); + const quads: ResultStream = await engine.queryQuads({ mock: 'algebra' }); + // @ts-ignore + const bool: boolean = await engine.queryBoolean({ mock: 'algebra' }); // Unsupported + // @ts-ignore + const done: void = await engine.queryVoid({ mock: 'algebra' }); // Unsupported +} diff --git a/stream.d.ts b/stream.d.ts index bc78df6..2217114 100644 --- a/stream.d.ts +++ b/stream.d.ts @@ -6,6 +6,7 @@ import { EventEmitter } from "events"; import { BaseQuad, Quad, Term } from './data-model'; +// TODO: merge this with ResultStream upon the next major change /** * A quad stream. * This stream is only readable, not writable. diff --git a/tsconfig.json b/tsconfig.json index f7c711a..e996f9e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,10 +11,12 @@ "strictNullChecks": true, "strictFunctionTypes": true, "noEmit": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "downlevelIteration": true }, "files": [ "index.d.ts", - "rdf-js-tests.ts" + "rdf-js-tests.ts", + "rdf-js-query-tests.ts" ] }