@comake/skl-js-engine
Version:
Standard Knowledge Language Javascript Engine
1,408 lines (1,321 loc) • 55.3 kB
text/typescript
/* eslint-disable no-inline-comments */
/* eslint-disable line-comment-position */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable multiline-comment-style */
/* eslint-disable capitalized-comments */
/* eslint-disable indent */
import DataFactory from '@rdfjs/data-model';
import type { Literal, NamedNode, Term, Variable } from '@rdfjs/types';
import type {
BindPattern,
ConstructQuery,
Expression,
FilterPattern,
GraphPattern,
Grouping,
IriTerm,
OperationExpression,
Ordering,
Pattern,
PropertyPath,
SelectQuery,
Triple,
ValuePatternRow,
ValuesPattern
} from 'sparqljs';
import { EngineConstants, XSD } from '../../../constants';
import { PerformanceLogger } from '../../../util/PerformanceLogger';
import {
allTypesAndSuperTypesPath,
createFilterPatternFromFilters,
createSparqlBasicGraphPattern,
createSparqlConstructQuery,
createSparqlContainsOperation,
createSparqlEqualOperation,
createSparqlExistsOperation,
createSparqlFilterWithExpression,
createSparqlGraphPattern,
createSparqlGteOperation,
createSparqlGtOperation,
createSparqlInOperation,
createSparqlInversePredicate,
createSparqlLcaseOperation,
createSparqlLteOperation,
createSparqlLtOperation,
createSparqlNotEqualOperation,
createSparqlNotExistsOperation,
createSparqlNotInOperation,
createSparqlOneOrMorePredicate,
createSparqlOptional,
createSparqlSelectGroup,
createSparqlSelectQuery,
createSparqlSequencePredicate,
createSparqlServicePattern,
createSparqlUnion,
createSparqlZeroOrMorePredicate,
entityGraphTriple,
entityVariable
} from '../../../util/SparqlUtil';
import { valueToLiteral } from '../../../util/TripleUtil';
import type { OrArray } from '../../../util/Types';
import { isUrl } from '../../../util/Util';
import { RDF } from '../../../util/Vocabularies';
import { FindOperator } from '../../FindOperator';
import type {
FieldPrimitiveValue,
FindOptionsOrder,
FindOptionsOrderValue,
FindOptionsRelations,
FindOptionsSelect,
FindOptionsWhere,
FindOptionsWhereField,
IdFindOptionsWhereField,
SubQuery,
TypeFindOptionsWhereField,
ValueWhereFieldObject
} from '../../FindOptionsTypes';
import type { GroupByOptions } from '../../GroupOptionTypes';
import type { InverseRelationOperatorValue } from '../../operator/InverseRelation';
import type { InverseRelationOrderValue } from '../../operator/InverseRelationOrder';
import { VariableGenerator } from './VariableGenerator';
export interface NonGraphWhereQueryData {
values: ValuesPattern[];
triples: Triple[];
filters: OperationExpression[];
patterns?: Pattern[];
binds?: Pattern[];
}
export interface WhereQueryData extends NonGraphWhereQueryData {
graphValues: ValuesPattern[];
graphTriples: Triple[];
graphFilters: OperationExpression[];
serviceTriples?: Record<string, Triple[]>;
selectVariables?: { variable: Variable; }[];
}
export interface RelationsQueryData {
patterns: Pattern[];
selectionTriples: Triple[];
unionPatterns: Pattern[];
}
export interface OrderQueryData {
triples: Triple[];
filters: OperationExpression[];
orders: Ordering[];
groupByParent?: boolean;
patterns?: Pattern[];
}
export interface EntitySelectQueryData {
where: Pattern[];
orders: Ordering[];
graphWhere: Pattern[];
graphSelectionTriples: Triple[];
group?: Variable;
selectVariables: { variable: Variable; expression: Expression }[];
relationsQueryData?: RelationsQueryData;
}
export interface SparqlQueryBuilderOptions {
where?: FindOptionsWhere;
select?: FindOptionsSelect;
order?: FindOptionsOrder;
relations?: FindOptionsRelations;
subQueries?: SubQuery[];
}
export class SparqlQueryBuilder {
private readonly variableGenerator: VariableGenerator;
public constructor() {
this.variableGenerator = new VariableGenerator();
}
public buildEntitySelectPatternsFromOptions(
subject: Variable,
options?: SparqlQueryBuilderOptions
): EntitySelectQueryData {
const span = PerformanceLogger.startSpan('QueryBuilder.buildSelect', {
hasWhere: !!options?.where,
hasRelations: !!options?.relations
});
try {
const relations = options?.select ? undefined : options?.relations;
const whereQueryData = this.createWhereQueryData(subject, options?.where, true);
const orderQueryData = this.createOrderQueryData(subject, options?.order);
const relationsQueryData = this.createRelationsQueryData(subject, relations);
// Handle subqueries
if (options?.subQueries && options.subQueries.length > 0) {
const subQueryPatterns = this.createSubQueryPatterns(options.subQueries);
whereQueryData.values.unshift(...(subQueryPatterns as ValuesPattern[]));
}
const patterns: Pattern[] = whereQueryData.values;
if (
whereQueryData.triples.length === 0 &&
(whereQueryData.filters.length > 0 ||
orderQueryData.triples.length > 0 ||
(whereQueryData.values.length === 0 &&
whereQueryData.graphValues.length === 0 &&
whereQueryData.graphTriples.length === 0))
) {
if (relationsQueryData.unionPatterns.length > 0) {
/* relationsQueryData.unionPatterns.push(
createSparqlGraphPattern(subject, [ createSparqlBasicGraphPattern([ entityGraphTriple ]) ])
); */
} else {
const entityGraphFilterPattern = this.createEntityGraphFilterPattern(subject);
// patterns.push(createSparqlGraphPattern(subject, [ createSparqlBasicGraphPattern([ entityGraphTriple ]) ]));
patterns.push(entityGraphFilterPattern);
}
} else if (!options?.where?.id) {
const entityGraphFilterPattern = this.createEntityGraphFilterPattern(subject);
const entityIsGraphFilter = createSparqlExistsOperation([ entityGraphFilterPattern ]);
whereQueryData.filters.push(entityIsGraphFilter);
}
// Add union patterns to the patterns
if (relationsQueryData.unionPatterns.length > 0) {
patterns.push(createSparqlUnion(relationsQueryData.unionPatterns));
}
const wherePatterns = this.createWherePatternsFromQueryData(
patterns,
whereQueryData.triples,
whereQueryData.filters,
orderQueryData.triples,
orderQueryData.filters,
whereQueryData.patterns ?? [],
undefined,
whereQueryData.binds
);
// For ID-only queries, we need to include union patterns in graphWhere
// because that's what gets used in the CONSTRUCT query
const graphWhereRelationsPatterns = relationsQueryData.unionPatterns.length > 0
? [ createSparqlUnion(relationsQueryData.unionPatterns), ...relationsQueryData.patterns ]
: relationsQueryData.patterns;
const graphWherePatterns = this.createWherePatternsFromQueryData(
whereQueryData.graphValues,
whereQueryData.graphTriples,
whereQueryData.graphFilters,
undefined,
undefined,
graphWhereRelationsPatterns
);
// Create variables for each order expression and update the orders to use them
const selectVariables = orderQueryData.orders.map(order => {
const variable = this.createVariable();
return {
variable,
expression: order.expression
};
});
const orders = selectVariables.map((selectVar, index) => ({
expression: selectVar.variable,
descending: orderQueryData.orders[index].descending
}));
if (orders.length === 0) {
orders.push({
expression: entityVariable
} as any);
}
selectVariables.push(...(whereQueryData.selectVariables ?? []) as { variable: Variable; expression: Expression }[]);
const isRelationsQueryDataEmpty = relationsQueryData.unionPatterns.length === 0 && relationsQueryData.patterns.length === 0 && relationsQueryData.selectionTriples.length === 0;
const returnData: any = {
where: wherePatterns,
orders,
...orderQueryData.groupByParent ? { group: subject } : {},
graphWhere: graphWherePatterns,
graphSelectionTriples: relationsQueryData.selectionTriples,
...isRelationsQueryDataEmpty ? { } : { relationsQueryData }
};
if (selectVariables.length > 0) {
returnData.selectVariables = selectVariables;
}
PerformanceLogger.endSpan(span, {
patternCount: wherePatterns.length,
tripleCount: whereQueryData.triples.length,
hasFilters: whereQueryData.filters.length > 0,
hasOrders: orders.length > 0
});
return returnData;
} catch (error) {
PerformanceLogger.endSpan(span, { error: true });
throw error;
}
}
private createSubQueryPatterns(subQueries: SubQuery[]): Pattern[] {
return subQueries.map((subQuery: SubQuery): Pattern => {
const subQueryWhere = this.createWhereQueryData(entityVariable, subQuery.where);
const queryGroup: Grouping[] = [];
if (subQuery.groupBy && Array.isArray(subQuery.groupBy)) {
subQuery.groupBy.forEach((group: string): void => {
queryGroup.push({
expression: DataFactory.variable(group)
});
});
}
const selectQuery: SelectQuery = {
type: 'query',
queryType: 'SELECT',
variables: subQuery.select,
where: this.createWherePatternsFromQueryData(
subQueryWhere.values,
subQueryWhere.triples,
subQueryWhere.filters,
undefined,
undefined,
subQueryWhere.patterns ?? []
),
group: queryGroup.length > 0 ? queryGroup : undefined,
having: subQuery.having ? this.createWhereQueryData(entityVariable, subQuery.having).filters : undefined,
prefixes: {}
};
return createSparqlSelectGroup([ selectQuery ]);
});
}
private createEntityGraphFilterPattern(subject: Variable): GraphPattern {
const entityFilterTriple = { subject, predicate: this.createVariable(), object: this.createVariable() };
return createSparqlGraphPattern(subject, [ createSparqlBasicGraphPattern([ entityFilterTriple ]) ]);
}
public buildConstructFromEntitySelectQuery(
graphWhere: Pattern[],
graphSelectionTriples: Triple[],
select?: FindOptionsSelect,
selectVariables?: { variable: Variable; expression: Expression }[]
): ConstructQuery {
const span = PerformanceLogger.startSpan('QueryBuilder.buildConstruct', { hasSelect: !!select });
try {
let triples: Triple[];
let where: Pattern[] = [];
if (select) {
triples = this.createSelectPattern(select, entityVariable);
where = [ createSparqlOptional([ createSparqlBasicGraphPattern(triples) ]), ...graphWhere ];
} else {
triples = [ entityGraphTriple, ...graphSelectionTriples ];
/* Skip if the where contains a union pattern */
if (graphWhere.some(pattern => pattern.type === 'union')) {
where = graphWhere;
} else {
where = [
...graphWhere,
createSparqlGraphPattern(entityVariable, [ createSparqlBasicGraphPattern([ entityGraphTriple ]) ])
];
}
}
// // Add select variables to the query
// if (selectVariables?.length) {
// where = [
// ...where,
// ...selectVariables.map(({ variable, expression }) => ({
// type: 'bind' as const,
// expression,
// variable,
// })),
// ];
// }
const result = createSparqlConstructQuery(triples, where);
PerformanceLogger.endSpan(span, { tripleCount: triples.length, patternCount: where.length });
return result;
} catch (error) {
PerformanceLogger.endSpan(span, { error: true });
throw error;
}
}
private createWhereQueryData(subject: Variable, where?: FindOptionsWhere, isTopLevel = false): WhereQueryData {
if (isTopLevel && Object.keys(where ?? {}).length === 1 && 'id' in where!) {
const { values, filters, triples } = this.createWhereQueryDataForIdValue(subject, where.id!);
return {
values: [],
filters: [],
triples: [],
graphValues: values,
graphFilters: filters,
graphTriples: triples,
binds: []
};
}
// Handle binds if specified in where options
const binds: Pattern[] = [];
if (where?.binds) {
binds.push(
...where.binds.map(
(bind): BindPattern => ({
type: 'bind' as const,
expression: bind.expression,
variable: bind.variable as Variable
})
)
);
// Delete binds from where as it's a special key
const { binds: _, ...restWhere } = where;
where = restWhere;
}
const whereQueryData = Object.entries(where ?? {}).reduce(
(obj: NonGraphWhereQueryData, [ key, value ]): NonGraphWhereQueryData => {
const whereQueryDataForField = this.createWhereQueryDataForField(subject, key, value!);
return {
values: [ ...obj.values, ...whereQueryDataForField.values ],
triples: [ ...obj.triples, ...whereQueryDataForField.triples ],
filters: [ ...obj.filters, ...whereQueryDataForField.filters ],
patterns: [ ...obj.patterns ?? [], ...whereQueryDataForField.patterns ?? [] ],
binds: [ ...obj.binds ?? [], ...whereQueryDataForField.binds ?? [] ]
};
},
{ values: [], triples: [], filters: [], patterns: [], binds }
);
return {
...whereQueryData,
graphValues: [],
graphFilters: [],
graphTriples: [],
patterns: whereQueryData.patterns ?? [],
binds: whereQueryData.binds ?? []
};
}
private createWhereQueryDataForField(
subject: Variable,
field: string,
value: IdFindOptionsWhereField | TypeFindOptionsWhereField | FindOptionsWhereField | FindOptionsWhere[]
): NonGraphWhereQueryData {
if (field === 'id') {
return this.createWhereQueryDataForIdValue(subject, value as FindOperator<any, any>);
}
if (field === 'type') {
return this.createWhereQueryDataForType(subject, value as FindOperator<any, any>);
}
if (field === 'and') {
return this.createWhereQueryDataForAndClause(subject, value as FindOptionsWhere[]);
}
if (field === 'or') {
return this.createWhereQueryDataForOrClause(subject, value as FindOptionsWhere[]);
}
const predicate = DataFactory.namedNode(field);
return this.createWhereQueryDataFromKeyValue(subject, predicate, value as FindOptionsWhereField);
}
private createWhereQueryDataForAndClause(
subject: Variable,
whereClauses: FindOptionsWhere[]
): NonGraphWhereQueryData {
// Combine all nested where conditions - all must match
return whereClauses.reduce(
(acc: NonGraphWhereQueryData, nestedWhere): NonGraphWhereQueryData => {
const nestedData = this.createWhereQueryData(subject, nestedWhere);
return {
values: [ ...acc.values, ...nestedData.values ],
triples: [ ...acc.triples, ...nestedData.triples ],
filters: [ ...acc.filters, ...nestedData.filters ],
patterns: [ ...acc.patterns ?? [], ...nestedData.patterns ?? [] ],
binds: [ ...acc.binds ?? [], ...nestedData.binds ?? [] ]
};
},
{ values: [], triples: [], filters: [], patterns: [], binds: [] }
);
}
private createWhereQueryDataForOrClause(
subject: Variable,
whereClauses: FindOptionsWhere[]
): NonGraphWhereQueryData {
// Create UNION patterns - any condition can match
const unionPatterns = whereClauses.map((nestedWhere): Pattern => {
const nestedData = this.createWhereQueryData(subject, nestedWhere);
const patterns: Pattern[] = [];
// Add values patterns
if (nestedData.values.length > 0) {
patterns.push(...nestedData.values);
}
// Add triples as basic graph pattern
if (nestedData.triples.length > 0) {
patterns.push(createSparqlBasicGraphPattern(nestedData.triples));
}
// Add nested patterns (for deeply nested and/or)
if (nestedData.patterns && nestedData.patterns.length > 0) {
patterns.push(...nestedData.patterns);
}
// Add filters
if (nestedData.filters.length > 0) {
patterns.push(createFilterPatternFromFilters(nestedData.filters));
}
// Add binds
if (nestedData.binds && nestedData.binds.length > 0) {
patterns.push(...nestedData.binds);
}
return createSparqlSelectGroup(patterns);
});
return {
values: [],
triples: [],
filters: [],
patterns: [ createSparqlUnion(unionPatterns) ],
binds: []
};
}
private createWhereQueryDataForIdValue(term: Variable, value: IdFindOptionsWhereField): NonGraphWhereQueryData {
let filters: OperationExpression[] = [];
let values: ValuesPattern[] = [];
let triples: Triple[] = [];
if (FindOperator.isFindOperator(value)) {
({ filters, values, triples } = this.resolveFindOperatorAsExpressionForId(
term,
value as FindOperator<string, any>
));
} else {
values = [
{
type: 'values',
values: [
{
[`?${term.value}`]: DataFactory.namedNode(value as string)
}
]
}
];
}
return {
values,
filters,
triples
};
}
private createWhereQueryDataForType(subject: Variable, value: TypeFindOptionsWhereField): NonGraphWhereQueryData {
if (FindOperator.isFindOperator(value)) {
if ((value as FindOperator<any, any>).operator === 'and') {
// For AND on types, generate a triple for each type
const typeValues = (value as unknown as FindOperator<any[], 'and'>).value;
// Create a triple for each value in the array, safely converting to string
const triples = (typeValues as any[]).map((typeVal: any) => {
const typeStr = typeof typeVal === 'string' ? typeVal : typeVal?.toString() || '';
return {
subject,
predicate: allTypesAndSuperTypesPath,
object: DataFactory.namedNode(typeStr)
};
});
return {
values: [],
filters: [],
triples: [
...triples
]
};
}
if ((value as FindOperator<any, any>).operator === 'inverse') {
const inversePredicate = createSparqlInversePredicate([ allTypesAndSuperTypesPath ]);
const inverseWhereQueryData = this.createWhereQueryDataFromKeyValue(
subject,
inversePredicate,
(value as FindOperator<any, any>).value
);
const variable = this.createVariable();
return {
values: inverseWhereQueryData.values,
filters: inverseWhereQueryData.filters,
triples: inverseWhereQueryData.triples
};
}
if ((value as FindOperator<any, any>).operator === 'sequence') {
const sequencePredicate = createSparqlSequencePredicate([ allTypesAndSuperTypesPath ]);
return this.createWhereQueryDataFromKeyValue(
subject,
sequencePredicate,
(value as FindOperator<any, any>).value
);
}
if (Array.isArray(value)) {
const triples = value.map(typeVal => ({
subject,
predicate: allTypesAndSuperTypesPath,
object:
typeof typeVal === 'string' ? DataFactory.namedNode(typeVal) : DataFactory.namedNode(typeVal.toString())
}));
return {
values: [],
filters: [],
triples: [
...triples
]
};
}
const variable = this.createVariable();
const triple = { subject, predicate: allTypesAndSuperTypesPath, object: variable };
const { filter, valuePattern, tripleInFilter } = this.resolveFindOperatorAsExpressionWithMultipleValues(
variable,
value as FindOperator<string, any>,
triple
);
return {
values: valuePattern ? [ valuePattern ] : [],
filters: filter ? [ filter ] : [],
triples: tripleInFilter ? [] : [ triple ]
};
}
return {
values: [],
filters: [],
triples: [
{
subject,
predicate: allTypesAndSuperTypesPath,
object: DataFactory.namedNode(value as string)
}
]
};
}
private createWhereQueryDataFromKeyValue(
subject: Variable,
predicate: IriTerm | PropertyPath,
value: FindOptionsWhereField
): NonGraphWhereQueryData {
if (Array.isArray(value) && FindOperator.isFindOperator(value[0])) {
return this.createWhereQueryDataForMultipleFindOperators(subject, predicate, value as FindOperator<any, any>[]);
}
if (FindOperator.isFindOperator(value)) {
return this.createWhereQueryDataForFindOperator(subject, predicate, value as FindOperator<any, any>);
}
if (Array.isArray(value)) {
return (value as FieldPrimitiveValue[]).reduce(
(obj: NonGraphWhereQueryData, valueItem): NonGraphWhereQueryData => {
const valueWhereQueryData = this.createWhereQueryDataFromKeyValue(subject, predicate, valueItem);
return {
values: [ ...obj.values, ...valueWhereQueryData.values ],
filters: [ ...obj.filters, ...valueWhereQueryData.filters ],
triples: [ ...obj.triples, ...valueWhereQueryData.triples ],
patterns: [ ...obj.patterns ?? [], ...valueWhereQueryData.patterns ?? [] ]
};
},
{ values: [], filters: [], triples: [], patterns: [] }
);
}
if (typeof value === 'object') {
if ('@value' in value) {
return this.createWhereQueryDataForValueObject(
'subject' in value ? (value.subject as unknown as Variable) : subject,
predicate,
value as ValueWhereFieldObject
);
}
return this.createWhereQueryDataForNestedWhere(subject, predicate, value as FindOptionsWhere);
}
const term = this.resolveValueToTerm(value);
return {
values: [],
filters: [],
triples: [{ subject, predicate, object: term }]
};
}
private createWhereQueryDataForFindOperator(
subject: Variable,
predicate: IriTerm | PropertyPath,
operator: FindOperator<any, any>
): NonGraphWhereQueryData {
if (operator.operator === 'inverse') {
const inversePredicate = createSparqlInversePredicate([ predicate ]);
return this.createWhereQueryDataFromKeyValue(
operator.subject ? operator.subject : subject,
inversePredicate,
operator.value
);
}
if (operator.operator === 'sequence') {
const sequencePredicate = createSparqlSequencePredicate([ predicate ]);
return this.createWhereQueryDataFromKeyValue(
operator.subject ? operator.subject : subject,
sequencePredicate,
operator.value
);
}
if (FindOperator.isPathOperator(operator)) {
const pathPredicate = this.pathOperatorToPropertyPath(operator);
const combinedPredicate = createSparqlSequencePredicate([ predicate, pathPredicate ]);
return this.createWhereQueryDataFromKeyValue(subject, combinedPredicate, operator.value.value);
}
const variable = this.createVariable();
const triple = { subject, predicate, object: variable };
const { filter, valuePattern, tripleInFilter } = this.resolveFindOperatorAsExpressionWithMultipleValues(
variable,
operator,
triple
);
return {
values: valuePattern ? [ valuePattern ] : [],
filters: filter ? [ filter ] : [],
triples: tripleInFilter ? [] : [ triple ]
};
}
private pathOperatorToPropertyPath(
operator: FindOperator<any, 'inversePath' | 'sequencePath' | 'zeroOrMorePath' | 'oneOrMorePath'>
): PropertyPath {
if (operator.operator === 'inversePath') {
let subPredicate: IriTerm | PropertyPath;
const { subPath } = operator.value;
if (typeof subPath === 'string') {
subPredicate = DataFactory.namedNode(subPath);
} else {
subPredicate = this.pathOperatorToPropertyPath(subPath);
}
return createSparqlInversePredicate([ subPredicate ]);
}
if (operator.operator === 'sequencePath') {
const { subPath } = operator.value;
const subPredicates = subPath.map((sequencePart: string | FindOperator<any, any>): IriTerm | PropertyPath => {
if (typeof sequencePart === 'string') {
return DataFactory.namedNode(sequencePart);
}
return this.pathOperatorToPropertyPath(sequencePart);
});
return createSparqlSequencePredicate(subPredicates);
}
if (operator.operator === 'zeroOrMorePath') {
const { subPath } = operator.value;
let subPredicate: IriTerm | PropertyPath;
if (typeof subPath === 'string') {
subPredicate = DataFactory.namedNode(subPath);
} else {
subPredicate = this.pathOperatorToPropertyPath(subPath);
}
return createSparqlZeroOrMorePredicate([ subPredicate ]);
}
if (operator.operator === 'oneOrMorePath') {
const { subPath } = operator.value;
let subPredicate: IriTerm | PropertyPath;
if (typeof subPath === 'string') {
subPredicate = DataFactory.namedNode(subPath);
} else {
subPredicate = this.pathOperatorToPropertyPath(subPath);
}
return createSparqlOneOrMorePredicate([ subPredicate ]);
}
throw new Error(`Operator ${operator.operator} not supported`);
}
private createWhereQueryDataForMultipleFindOperators(
subject: Variable,
predicate: IriTerm | PropertyPath,
operators: FindOperator<any, any>[]
): NonGraphWhereQueryData {
const variable = this.createVariable();
const triple = { subject, predicate, object: variable };
const whereQueryData = {
values: [],
filters: [],
triples: [ triple ]
};
return operators.reduce((obj: NonGraphWhereQueryData, operator): NonGraphWhereQueryData => {
const { filter, valuePattern } = this.resolveFindOperatorAsExpressionWithMultipleValues(
variable,
operator,
triple
);
if (valuePattern) {
obj.values.push(valuePattern);
}
if (filter) {
obj.filters.push(filter);
}
return obj;
}, whereQueryData);
}
private createWhereQueryDataForNestedWhere(
subject: Variable,
predicate: IriTerm | PropertyPath,
where: FindOptionsWhere
): NonGraphWhereQueryData {
const subNodeVariable = this.createVariable();
const subWhereQueryData = this.createWhereQueryData(subNodeVariable, where);
return {
values: [ ...subWhereQueryData.values, ...subWhereQueryData.graphValues ],
filters: subWhereQueryData.filters,
triples: [{ subject, predicate, object: subNodeVariable }, ...subWhereQueryData.triples ],
patterns: [ ...subWhereQueryData.patterns ?? [] ]
};
}
private createWhereQueryDataForValueObject(
subject: Variable,
predicate: IriTerm | PropertyPath,
valueObject: ValueWhereFieldObject
): NonGraphWhereQueryData {
const term = this.valueObjectToTerm(valueObject);
if ((valueObject as any).isOptional) {
return {
values: [],
filters: [],
triples: [],
patterns: [ createSparqlOptional([ createSparqlBasicGraphPattern([{ subject, predicate, object: term }]) ]) ]
};
}
return {
values: [],
filters: [],
triples: [{ subject, predicate, object: term }]
};
}
private valueObjectToTerm(valueObject: ValueWhereFieldObject): Literal | Variable {
let typeOrLanguage: string;
let value: string;
if ('@type' in valueObject && valueObject['@type'] === '@json') {
typeOrLanguage = RDF.JSON;
value = JSON.stringify(valueObject['@value']);
} else {
typeOrLanguage = ('@type' in valueObject ? valueObject['@type'] : valueObject['@language'])!;
value = (valueObject['@value'] as FieldPrimitiveValue).toString();
}
return valueToLiteral(value, typeOrLanguage);
}
private resolveFindOperatorAsExpressionWithMultipleValues(
leftSide: Variable,
operator: FindOperator<any, any>,
triple: Triple,
dontUseValuePattern = false
): { filter?: OperationExpression; valuePattern?: ValuesPattern; tripleInFilter?: boolean } {
if (operator.operator === 'and') {
// For AND operators, we'll create an AND filter with multiple equality comparisons
const values = operator.value as any[];
if (values.length === 0) {
// No conditions to add
return {};
}
if (values.length === 1) {
// If only one value, treat as equality
return {
filter: createSparqlEqualOperation(leftSide, this.resolveValueToExpression(values[0]) as Expression)
};
}
// Create individual equality conditions for each value
const equalityConditions: OperationExpression[] = values.map(val =>
createSparqlEqualOperation(leftSide, this.resolveValueToExpression(val) as Expression));
// Combine with AND operation
return {
filter: {
type: 'operation',
operator: '&&',
args: equalityConditions
}
};
}
if (operator.operator === 'in') {
const resolvedValue = this.resolveValueToExpression(operator.value) as (NamedNode | Literal)[];
if (Array.isArray(resolvedValue) && !dontUseValuePattern) {
return {
valuePattern: {
type: 'values',
values: resolvedValue.map((value): ValuePatternRow => ({ [`?${leftSide.value}`]: value }))
}
};
}
return {
filter: createSparqlInOperation(leftSide, resolvedValue as Expression)
};
}
if (operator.operator === 'not') {
const resolvedExpression = this.resolveValueToExpression(operator.value) as Expression | FindOperator<any, any>;
return {
filter: this.buildNotOperationForMultiValued(leftSide, resolvedExpression, triple),
tripleInFilter: true
};
}
if (operator.operator === 'exists') {
return {
filter: createSparqlExistsOperation([ createSparqlBasicGraphPattern([ triple ]) ]),
tripleInFilter: true
};
}
if (operator.operator === 'contains') {
const searchString = this.resolveValueToExpression(operator.value) as Literal;
const filter = createSparqlContainsOperation(
// Directly use the variable as an expression
createSparqlLcaseOperation(DataFactory.variable(leftSide.value)),
createSparqlLcaseOperation(DataFactory.literal(searchString.value.toLowerCase()))
);
return {
filter
};
}
const resolvedExpression = this.resolveValueToExpression(operator.value) as Expression;
switch (operator.operator) {
case 'equal':
return { filter: createSparqlEqualOperation(leftSide, resolvedExpression) };
case 'gt':
return { filter: createSparqlGtOperation(leftSide, resolvedExpression) };
case 'gte':
return { filter: createSparqlGteOperation(leftSide, resolvedExpression) };
case 'lt':
return { filter: createSparqlLtOperation(leftSide, resolvedExpression) };
case 'lte':
return { filter: createSparqlLteOperation(leftSide, resolvedExpression) };
default:
throw new Error(`Unsupported operator "${operator.operator}"`);
}
}
private resolveFindOperatorAsExpressionForId(
leftSide: Variable,
operator: FindOperator<any, any>
): NonGraphWhereQueryData {
switch (operator.operator) {
case 'inversePath': {
const predicate = this.pathOperatorToPropertyPath(operator);
return this.createWhereQueryDataFromKeyValue(leftSide, predicate, operator.value.value);
}
case 'in': {
const resolvedValue = this.resolveValueToExpression(operator.value) as NamedNode[];
return {
triples: [],
filters: [],
values: [
{
type: 'values',
values: resolvedValue.map((value): ValuePatternRow => ({ [`?${leftSide.value}`]: value }))
}
]
};
}
case 'not':
return {
triples: [],
values: [],
filters: [
this.buildNotOperationForId(
leftSide,
this.resolveValueToExpression(operator.value) as Expression | FindOperator<any, any>
)
]
};
case 'equal':
return {
triples: [],
values: [],
filters: [ createSparqlEqualOperation(leftSide, this.resolveValueToExpression(operator.value) as Expression) ]
};
default:
throw new Error(`Unsupported operator "${operator.operator}"`);
}
}
private resolveValueToExpression(
value: OrArray<any> | FindOperator<any, any>
): FindOperator<any, any> | OrArray<Term> {
if (FindOperator.isFindOperator(value)) {
return value;
}
if (Array.isArray(value)) {
return value.map((valueItem): Term => this.resolveValueToTerm(valueItem));
}
return this.resolveValueToTerm(value);
}
private buildNotOperationForMultiValued(
leftSide: Variable,
rightSide: Expression | FindOperator<any, any>,
triple: Triple
): OperationExpression {
let filterExpression: FilterPattern;
const isFindOperator = FindOperator.isFindOperator(rightSide);
if (isFindOperator && (rightSide as FindOperator<any, any>).operator === 'exists') {
return createSparqlNotExistsOperation([ createSparqlBasicGraphPattern([ triple ]) ]);
}
if (isFindOperator) {
let expression: OperationExpression | undefined;
try {
({ filter: expression } = this.resolveFindOperatorAsExpressionWithMultipleValues(
leftSide,
rightSide as FindOperator<any, any>,
triple,
true
));
} catch {
throw new Error(`Unsupported Not sub operator "${(rightSide as FindOperator<any, any>).operator}"`);
}
filterExpression = createSparqlFilterWithExpression(expression!);
} else {
filterExpression = createSparqlFilterWithExpression(
createSparqlEqualOperation(leftSide, rightSide as Expression)
);
}
return createSparqlNotExistsOperation([
createSparqlSelectGroup([ createSparqlBasicGraphPattern([ triple ]), filterExpression ])
]);
}
private buildNotOperationForId(
leftSide: Expression,
rightSide: Expression | FindOperator<any, any>
): OperationExpression {
if (FindOperator.isFindOperator(rightSide)) {
const resolvedValue = this.resolveValueToExpression((rightSide as FindOperator<string, any>).value) as Expression;
switch ((rightSide as FindOperator<string, any>).operator) {
case 'in':
return createSparqlNotInOperation(leftSide, resolvedValue);
case 'equal':
return createSparqlNotEqualOperation(leftSide, resolvedValue);
default:
throw new Error(`Unsupported Not sub operator "${(rightSide as FindOperator<string, any>).operator}"`);
}
}
return createSparqlNotEqualOperation(leftSide, rightSide as Expression);
}
private resolveValueToTerm(value: FieldPrimitiveValue | ValueWhereFieldObject): NamedNode | Literal | Variable {
if (typeof value === 'object' && '@value' in value) {
return valueToLiteral((value as ValueWhereFieldObject)['@value'], '@type' in value ? value['@type'] : undefined);
}
if (isUrl(value)) {
return DataFactory.namedNode(value as string);
}
return valueToLiteral(value);
}
private createOrderQueryData(
subject: Variable,
order?: FindOptionsOrder | FindOperator<InverseRelationOrderValue, 'inverseRelationOrder'>,
isNested = false
): OrderQueryData {
if (!order) {
// Default is descending by id
return { triples: [], orders: [ ], filters: []};
}
return Object.entries(order).reduce(
(obj: OrderQueryData, [ property, orderValue ]): OrderQueryData => {
const orderQueryData = this.createOrderQueryDataForProperty(subject, property, orderValue, isNested);
obj.orders = [ ...obj.orders, ...orderQueryData.orders ];
obj.triples = [ ...obj.triples, ...orderQueryData.triples ];
obj.filters = [ ...obj.filters, ...orderQueryData.filters ];
obj.groupByParent = obj.groupByParent ?? orderQueryData.groupByParent;
return obj;
},
{ triples: [], orders: [], filters: []}
);
}
private createOrderQueryDataForProperty(
subject: Variable,
property: string,
orderValue: FindOptionsOrderValue | FindOperator<InverseRelationOrderValue, 'inverseRelationOrder'>,
isNested = false
): OrderQueryData {
const predicate = DataFactory.namedNode(property);
if (FindOperator.isFindOperator(orderValue)) {
const variable = this.createVariable();
const inverseRelationTriple = {
subject,
predicate: createSparqlInversePredicate([ predicate ]),
object: variable
};
const subRelationOperatorValue = (orderValue as FindOperator<InverseRelationOrderValue, 'inverseRelationOrder'>)
.value as InverseRelationOrderValue;
const subRelationOrderQueryData = this.createOrderQueryData(variable, subRelationOperatorValue.order, true);
const subRelationWhereQueryData = this.createWhereQueryData(variable, subRelationOperatorValue.where);
// Create aggregate expressions for each order, but don't nest aggregates
const aggregateOrders = subRelationOrderQueryData.orders.map(order => {
const baseExpression =
'type' in order.expression && (order.expression as any).type === 'aggregate'
? (order.expression as any).expression
: order.expression;
// Create the aggregate expression first
const aggregateExpression = {
type: 'aggregate',
expression: baseExpression,
aggregation: order.descending ? 'max' : 'min'
} as Expression;
return {
expression: aggregateExpression,
descending: order.descending
};
});
return {
triples: [ inverseRelationTriple, ...subRelationOrderQueryData.triples, ...subRelationWhereQueryData.triples ],
filters: subRelationWhereQueryData.filters,
orders: aggregateOrders,
groupByParent: true,
patterns: [ ...subRelationWhereQueryData.patterns ?? [], ...subRelationOrderQueryData.patterns ?? [] ]
};
}
if (property === 'id') {
return {
triples: [],
filters: [],
orders: [
{
expression: subject,
descending: orderValue === 'DESC' || orderValue === 'desc'
}
]
};
}
const variable = this.createVariable();
const isDescending = orderValue === 'DESC' || orderValue === 'desc';
return {
triples: [{ subject, predicate, object: variable }],
filters: [],
orders: [
{
expression: isNested
? {
type: 'aggregate',
expression: variable,
aggregation: isDescending ? 'max' : 'min'
}
: variable,
descending: isDescending
}
]
};
}
private createRelationsQueryData(subject: Variable, relations?: FindOptionsRelations): RelationsQueryData {
if (!relations) {
return { patterns: [], selectionTriples: [], unionPatterns: []};
}
const response = Object.entries(relations).reduce(
(
obj: RelationsQueryData & { unionPatterns?: Pattern[] },
[ property, relationsValue ]
): RelationsQueryData & { unionPatterns?: Pattern[] } => {
const predicate = DataFactory.namedNode(property);
if (typeof relationsValue === 'object') {
if (FindOperator.isFindOperator(relationsValue)) {
const { patterns, selectionTriples, unionPatterns } = this.createRelationsQueryDataForInverseRelation(
subject,
predicate,
relationsValue as FindOperator<InverseRelationOperatorValue, 'inverseRelation'>
);
return {
patterns: [ ...obj.patterns, ...patterns ],
selectionTriples: [ ...obj.selectionTriples, ...selectionTriples ],
unionPatterns: [ ...obj.unionPatterns ?? [], ...unionPatterns ?? [] ]
};
}
const { patterns, selectionTriples, unionPatterns } = this.createRelationsQueryDataForNestedRelation(
subject,
predicate,
relationsValue as FindOptionsRelations
);
return {
patterns: [ ...obj.patterns, ...patterns ],
selectionTriples: [ ...obj.selectionTriples, ...selectionTriples ],
unionPatterns: [
...obj.unionPatterns ?? [],
...unionPatterns ?? []
]
};
}
const variable = this.createVariable();
const relationPattern = createSparqlSelectGroup([
createSparqlBasicGraphPattern([{ subject, predicate, object: variable }]),
createSparqlGraphPattern(variable, [ createSparqlBasicGraphPattern([ entityGraphTriple ]) ])
]);
return {
unionPatterns: [ ...obj.unionPatterns ?? [], relationPattern ],
patterns: [ ...obj.patterns ],
selectionTriples: [ ...obj.selectionTriples ]
};
},
{ patterns: [], selectionTriples: [], unionPatterns: []}
);
return {
patterns: [ ...response?.patterns ?? [] ],
selectionTriples: [ ...response?.selectionTriples ?? [] ],
unionPatterns: [ ...response?.unionPatterns ?? [] ]
};
}
private createRelationsQueryDataForInverseRelation(
subject: Variable,
predicate: NamedNode,
relationsValue: FindOperator<InverseRelationOperatorValue, 'inverseRelation'>
): RelationsQueryData {
const variable = this.createVariable();
const inverseRelationTriple = {
subject,
predicate: createSparqlInversePredicate([ predicate ]),
object: variable
};
if (typeof relationsValue.value === 'object' && (relationsValue.value as InverseRelationOperatorValue).relations) {
const subRelationsQueryData = this.createRelationsQueryData(
variable,
(relationsValue.value as InverseRelationOperatorValue).relations
);
const unionPatterns: Pattern[] = [];
unionPatterns.push(createSparqlSelectGroup([
createSparqlBasicGraphPattern([ inverseRelationTriple ]),
createSparqlGraphPattern(variable, [ createSparqlBasicGraphPattern([ entityGraphTriple ]) ])
]));
for (const subRelationUnionPattern of subRelationsQueryData.unionPatterns) {
unionPatterns.push(
createSparqlSelectGroup(
[
createSparqlBasicGraphPattern([ inverseRelationTriple ]),
...subRelationUnionPattern.type === 'group'
? subRelationUnionPattern.patterns
: [ subRelationUnionPattern ]
]
)
);
}
return {
patterns: [ ],
selectionTriples: [ ...subRelationsQueryData.selectionTriples ],
unionPatterns
};
}
const unionPatterns: Pattern[] = [];
unionPatterns.push(createSparqlSelectGroup([
createSparqlBasicGraphPattern([ inverseRelationTriple ]),
createSparqlGraphPattern(variable, [ createSparqlBasicGraphPattern([ entityGraphTriple ]) ])
]));
return {
patterns: [ ],
selectionTriples: [ ],
unionPatterns
};
}
private createRelationsQueryDataForNestedRelation(
subject: Variable,
predicate: NamedNode,
relationsValue: FindOptionsRelations
): RelationsQueryData {
const variable = this.createVariable();
const relationTriple = { subject, predicate, object: variable };
const unionPatterns: Pattern[] = [];
const subRelationsQueryData = this.createRelationsQueryData(variable, relationsValue);
// subRelationsQueryData.patterns.push(createSparqlBasicGraphPattern([ relationTriple ]));
// unionPatterns.push(...subRelationsQueryData.patterns);
// unionPatterns.push(
// createSparqlGraphPattern(variable, [ createSparqlBasicGraphPattern([ entityGraphTriple ]) ])
// );
/* const relationPattern = subRelationsQueryData.patterns.length > 0
? createSparqlSelectGroup([
...patterns,
...subRelationsQueryData.patterns,
...subRelationsQueryData.unionPatterns
])
: undefined; */
/* We always need to include the first level of the property in the union patterns */
unionPatterns.push(
createSparqlSelectGroup([
createSparqlBasicGraphPattern([ relationTriple ]),
createSparqlGraphPattern(variable, [ createSparqlBasicGraphPattern([ entityGraphTriple ]) ])
])
);
for (const subRelationUnionPattern of subRelationsQueryData.unionPatterns) {
unionPatterns.push(
createSparqlSelectGroup(
[
createSparqlBasicGraphPattern([ relationTriple ]),
...subRelationUnionPattern.type === 'group' ? subRelationUnionPattern.patterns : [ subRelationUnionPattern ]
]
)
);
}
return {
patterns: [ ],
selectionTriples: [ ...subRelationsQueryData.selectionTriples ],
unionPatterns
};
}
private createVariable(): Variable {
return DataFactory.variable(this.variableGenerator.getNext());
}
private createSelectPattern(select: FindOptionsSelect, subject: Variable): Triple[] {
if (Array.isArray(select)) {
return select.map(
(selectPredicate): Triple => ({
subject,
predicate: DataFactory.namedNode(selectPredicate),
object: this.createVariable()
})
);
}
return Object.entries(select).reduce((arr: Triple[], [ key, value ]): Triple[] => {
const variable = this.createVariable();
arr.push({ subject, predicate: DataFactory.namedNode(key), object: variable });
if (typeof value === 'object') {
arr = [ ...arr, ...this.createSelectPattern(value, variable) ];
}
return arr;
}, []);
}
private createWherePatternsFromQueryData(
initialPatterns: Pattern[],
triples: Triple[],
filters: OperationExpression[],
orderTriples?: Triple[],
orderFilters?: OperationExpression[],
additionalPatterns?: Pattern[],
serviceTriples?: Record<string, Triple[]>,
binds?: Pattern[]
): Pattern[] {
let patterns = initialPatterns;
// Add binds at the beginning if they exist
if (binds && binds.length > 0) {
patterns = [ ...patterns, ...binds ];
}
if (triples.length > 0) {
patterns.push(createSparqlBasicGraphPattern(triples));
}
if (orderTriples && orderTriples.length > 0) {
const optionalPatterns: Pattern[] = [ createSparqlBasicGraphPattern(orderTriples) ];
if (orderFilters && orderFilters.length > 0) {
optionalPatterns.push(createFilterPatternFromFilters(orderFilters));
}
patterns.push(createSparqlOptional(optionalPatterns));
}
if (filters.length > 0) {
patterns.push(createFilterPatternFromFilters(filters));
}
if (serviceTriples) {
for (const [ service, sTriples ] of Object.entries(serviceTriples)) {
patterns.unshift(createSparqlServicePattern(service, sTriples));
}
}
if (additionalPatterns) {
patterns = [ ...patterns, ...additionalPatterns ];
}
return patterns;
}
private createGroupPatternForPath(
entityVariable: Variable,
path: string
): {
variable: Variable;
patterns: Pattern[];
} {
const segments = path.split('~');
let currentSubject = entityVariable;
const patterns: Pattern[] = [];
// Create a chain of patterns for each segment
segments.forEach((predicate, index) => {
const object = this.createVariable();
patterns.push({
type: 'bgp',
triples: [
{
subject: currentSubject,
predicate: DataFactory.namedNode(predicate),
object
}
]
});
currentSubject = object;
});
// Return the final variable (last object) and all patterns
return {
variable: currentSubject,
patterns
};
}
public async buildGroupByQuery(
options: GroupByOptions
): Promise<{ query: SelectQuery; variableMapping: Record<string, string> }> {
const span = PerformanceLogger.startSpan('QueryBuilder.buildGroupBy', {
hasGroupBy: !!options.groupBy,
hasDateRange: !!options.dateRange
});
try {
const entityVariable = DataFactory.variable('entity');
const queryData = this.buildEntitySelectPatternsFromOptions(entityVariable, {
where: options.where || {}
});
// Add group variables and patterns with mapping
const groupVariables: Variable[] = [];
const groupPatterns: Pattern[] = [];
const variableMapping: Record<string, string> = {};
if (options.groupBy) {
options.groupBy.forEach(path => {
const { variable: groupVar, patterns } = this.createGroupPatternForPath(entityVariable, path);
groupVariables.push(groupVar);
variableMapping[groupVar.value] = path;
groupPatterns.push(...patterns);
});
}
// Add date handling if specified
if (options.dateRange) {
const dateVar = this.createVariable();
variableMapping[dateVar.value] = 'date';
const datePattern: Pattern = {
type: 'bgp',
triples: [
{
subject: entityVar