Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions engines/parser-sparql-1-2/lib/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export const sparql12ParserBuilder = ParserBuilder.create(sparql11ParserBuilder)
S12.buildInPredicate,
S12.buildInObject,
)
.patchRule(S12.selectQuery)
.patchRule(S12.dataBlockValue)
.patchRule(S12.triplesSameSubject)
.patchRule(S12.triplesSameSubjectPath)
Expand Down
3 changes: 3 additions & 0 deletions packages/rules-sparql-1-1/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ export * from './Sparql11types.js';
export * from './MinimalSparqlParser.js';
export * from './sparql11HelperTypes.js';
export * from './astFactory.js';
// TODO: deprecate this export
export * from './validation/validators.js';

export * as validation from './validation/validators.js';
export * from './utils.js';
6 changes: 3 additions & 3 deletions packages/rules-sparql-1-1/lib/validation/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const transformer = new AstTransformer();
/**
* Get all 'aggregate' rules from an expression
*/
function getAggregatesOfExpression(expression: Expression): ExpressionAggregate[] {
export function getAggregatesOfExpression(expression: Expression): ExpressionAggregate[] {
if (F.isExpressionAggregate(expression)) {
return [ expression ];
}
Expand All @@ -36,7 +36,7 @@ function getAggregatesOfExpression(expression: Expression): ExpressionAggregate[
/**
* Return the variable value id of an expression if bounded
*/
function getExpressionId(expression: SolutionModifierGroupBind | Expression | TermVariable): string | undefined {
export function getExpressionId(expression: SolutionModifierGroupBind | Expression | TermVariable): string | undefined {
// Check if grouping
if (F.isTerm(expression) && F.isTermVariable(expression)) {
return expression.value;
Expand All @@ -53,7 +53,7 @@ function getExpressionId(expression: SolutionModifierGroupBind | Expression | Te
/**
* Get all variables used in an expression
*/
function getVariablesFromExpression(expression: Expression, variables: Set<string>): void {
export function getVariablesFromExpression(expression: Expression, variables: Set<string>): void {
if (F.isExpressionOperator(expression)) {
for (const expr of expression.args) {
getVariablesFromExpression(expr, variables);
Expand Down
39 changes: 38 additions & 1 deletion packages/rules-sparql-1-2/lib/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
GraphNode,
GraphTerm,
PatternBgp,
QuerySelect,
Term,
TermBlank,
TermIri,
Expand All @@ -28,7 +29,7 @@ import type {
TripleCollectionReifiedTriple,
TripleNesting,
} from './sparql12Types.js';
import { langTagHasCorrectRange } from './validators.js';
import { langTagHasCorrectRange, queryProjectionIsGood } from './validators.js';

/**
*[[7]](https://www.w3.org/TR/sparql12-query/#rVersionDecl)
Expand All @@ -48,6 +49,42 @@ export const versionDecl: SparqlRule<'versionDecl', ContextDefinitionVersion> =
},
};

/**
* [[9]](https://www.w3.org/TR/sparql12-query/#rSelectQuery)
* (Validator has changed: https://github.com/w3c/sparql-query/pull/380)
*/
export const selectQuery: SparqlGrammarRule<'selectQuery', Omit<QuerySelect, 'type' | 'context' | 'values'>> = <const> {
name: 'selectQuery',
impl: ({ ACTION, SUBRULE }) => (C) => {
const selectVal = SUBRULE(S11.selectClause);
const from = SUBRULE(S11.datasetClauseStar);
const where = SUBRULE(S11.whereClause);
const modifiers = SUBRULE(S11.solutionModifier);

return ACTION(() => {
const ret = {
subType: 'select',
where: where.val,
solutionModifiers: modifiers,
datasets: from,
...selectVal.val,
loc: C.astFactory.sourceLocation(
selectVal,
where,
modifiers.group,
modifiers.having,
modifiers.order,
modifiers.limitOffset,
),
} satisfies RuleDefReturn<typeof selectQuery>;
if (!C.skipValidation) {
queryProjectionIsGood(ret);
}
return ret;
});
},
};

/**
* [[8]](https://www.w3.org/TR/sparql12-query/#rVersionSpecifier)
*/
Expand Down
12 changes: 12 additions & 0 deletions packages/rules-sparql-1-2/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import {
validation as validation11,
} from '@traqula/rules-sparql-1-1';
import * as validation12 from './validators.js';

export * as gram from './grammar.js';
export * as lex from './lexer.js';
export * from './sparql12Types.js';
export * from './sparql12HelperTypes.js';
export * from './parserUtils.js';
export * from './AstFactory.js';
// TODO: deprecate this export
export * from './validators.js';

type OverriddenKeys = keyof typeof validation11 & keyof typeof validation12;
export const validation = <Omit<typeof validation11, OverriddenKeys> & typeof validation12> {
...validation11,
...validation12,
};
85 changes: 85 additions & 0 deletions packages/rules-sparql-1-2/lib/validators.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { getAggregatesOfExpression, getExpressionId, getVariablesFromExpression } from '@traqula/rules-sparql-1-1';
import type * as T11 from '@traqula/rules-sparql-1-1';
import { AstFactory } from './AstFactory.js';
import type {
Path,
Pattern,
PatternBind,
QuerySelect,
SparqlQuery,
Term,
TermLiteral,
TermVariable,
TripleCollection,
TripleNesting,
Wildcard,
Expand Down Expand Up @@ -104,3 +109,83 @@ export function findPatternBoundedVars(
}
}
}

/**
* Verify that the projected variables (select head) are allowed:
* - no group-by on select *
* - if group-by, selected variables need to be collected by the group-by
* - 'select ?var as ?other', ?other cannot be in scope
*/
export function queryProjectionIsGood(query: Pick<QuerySelect, 'variables' | 'solutionModifiers' | 'where'>): void {
// NoGroupByOnWildcardSelect
if (query.variables.length === 1 && F.isWildcard(query.variables[0])) {
if (query.solutionModifiers.group !== undefined) {
throw new Error('GROUP BY not allowed with wildcard');
}
return;
}

// CannotProjectUngroupedVars - can be skipped if `SELECT *`
// Check for projection of ungrouped variable
// Check can be skipped in case of wildcard select.
const variables = <Exclude<typeof query.variables, [Wildcard]>> query.variables;
const hasCountAggregate = variables.flatMap(
varVal => F.isTerm(varVal) ? [] : getAggregatesOfExpression(<T11.Expression> varVal.expression),
).some(agg => agg.aggregation === 'count' && !agg.expression.some(arg => F.isWildcard(arg)));
const groupBy = query.solutionModifiers.group;
if (hasCountAggregate || groupBy) {
// We have to check whether
// 1. Variables used in projection are usable given the group by clause
// 2. A selectCount will create an implicit group by clause.
// Variables bound by preceding (expr AS ?var) expressions are in scope for later expressions.
const asBoundVars = new Set<string>();
for (const selectVar of variables) {
if (F.isTerm(selectVar)) {
if (!groupBy || !groupBy.groupings.map(groupvar =>
getExpressionId(<T11.Expression | T11.SolutionModifierGroupBind> groupvar))
.includes((getExpressionId(selectVar)))) {
throw new Error('Variable not allowed in projection');
}
} else if (getAggregatesOfExpression(<T11.Expression> selectVar.expression).length === 0) {
// Current value binding does not use aggregates
const usedvars = new Set<string>();
getVariablesFromExpression(<T11.Expression> selectVar.expression, usedvars);
for (const usedvar of usedvars) {
// If the var is created within the select, it is fine.
if (asBoundVars.has(usedvar)) {
continue;
}
if (!groupBy || !groupBy.groupings.map(groupVar =>
getExpressionId(<T11.Expression | T11.SolutionModifierGroupBind>groupVar)).includes(usedvar)) {
throw new Error(`Use of ungrouped variable in projection of operation (?${usedvar})`);
}
}
}
if (!F.isTerm(selectVar)) {
// Register a var is created by a bind
asBoundVars.add(selectVar.variable.value);
}
}
}

// NOTE 12: Check if id of each AS-selected column is not yet bound by subquery
const subqueries = query.where.patterns.filter(pattern => pattern.type === 'query');
if (subqueries.length > 0) {
const selectBoundedVars = new Set<string>();
for (const variable of variables) {
if ('variable' in variable) {
selectBoundedVars.add(variable.variable.value);
}
}

// Look at in scope variables
const vars = subqueries.flatMap<TermVariable | PatternBind | Wildcard>(sub => sub.variables)
.map(v => F.isTerm(v) ? v.value : (F.isWildcard(v) ? '*' : v.variable.value));
const subqueryIds = new Set(vars);
for (const selectedVarId of selectBoundedVars) {
if (subqueryIds.has(selectedVarId)) {
throw new Error(`Target id of 'AS' (?${selectedVarId}) already used in subquery`);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
{
"type": "project",
"input": {
"type": "extend",
"input": {
"type": "extend",
"input": {
"type": "group",
"input": {
"type": "values",
"variables": [
{
"termType": "Variable",
"value": "v"
}
],
"bindings": [
{
"v": {
"termType": "Literal",
"value": "0",
"datatype": {
"termType": "NamedNode",
"value": "http://www.w3.org/2001/XMLSchema#integer"
}
}
},
{
"v": {
"termType": "Literal",
"value": "1",
"datatype": {
"termType": "NamedNode",
"value": "http://www.w3.org/2001/XMLSchema#integer"
}
}
},
{
"v": {
"termType": "Literal",
"value": "2",
"datatype": {
"termType": "NamedNode",
"value": "http://www.w3.org/2001/XMLSchema#integer"
}
}
},
{
"v": {
"termType": "Literal",
"value": "3",
"datatype": {
"termType": "NamedNode",
"value": "http://www.w3.org/2001/XMLSchema#integer"
}
}
}
]
},
"variables": [],
"aggregates": [
{
"type": "expression",
"subType": "aggregate",
"aggregator": "count",
"expression": {
"type": "expression",
"subType": "term",
"term": {
"termType": "Variable",
"value": "v"
}
},
"distinct": false,
"variable": {
"termType": "Variable",
"value": "var0"
}
}
]
},
"variable": {
"termType": "Variable",
"value": "count"
},
"expression": {
"type": "expression",
"subType": "term",
"term": {
"termType": "Variable",
"value": "var0"
}
}
},
"variable": {
"termType": "Variable",
"value": "countPlusOne"
},
"expression": {
"type": "expression",
"subType": "operator",
"operator": "+",
"args": [
{
"type": "expression",
"subType": "term",
"term": {
"termType": "Variable",
"value": "count"
}
},
{
"type": "expression",
"subType": "term",
"term": {
"termType": "Literal",
"value": "1",
"datatype": {
"termType": "NamedNode",
"value": "http://www.w3.org/2001/XMLSchema#integer"
}
}
}
]
}
},
"variables": [
{
"termType": "Variable",
"value": "count"
},
{
"termType": "Variable",
"value": "countPlusOne"
}
]
}
Loading
Loading