From 2b4123ec0a8af3cfd050c089df6f38a1ed8512e6 Mon Sep 17 00:00:00 2001 From: Jitse De Smet Date: Wed, 27 May 2026 08:47:46 +0000 Subject: [PATCH 1/6] feat: add select-variable-reuse test and allow AS-bound variable references Add support for referencing variables bound by preceding (expr AS ?var) expressions in subsequent SELECT expressions, per the updated SPARQL spec (w3c/sparql-query#380, w3c/rdf-tests#340). The query `SELECT (COUNT(?v) AS ?count) (?count + 1 AS ?countPlusOne) ...` was previously rejected with 'Use of ungrouped variable' because the validator did not recognize that ?count was in scope from an earlier AS binding. Changes: - Fix queryProjectionIsGood validation to track AS-bound variables and allow their use in subsequent projection expressions - Add select-variable-reuse test with AST, algebra, and generator statics Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../lib/validation/validators.ts | 8 + .../aggregates/select-variable-reuse.json | 137 ++++++++++ .../aggregates/select-variable-reuse.json | 137 ++++++++++ .../aggregates/select-variable-reuse.sparql | 8 + .../aggregates/select-variable-reuse.sparql | 8 + .../aggregates/select-variable-reuse.sparql | 3 + .../sparql-1-1/select-variable-reuse.json | 243 ++++++++++++++++++ .../sparql-1-1/select-variable-reuse.sparql | 1 + .../sparql-1-1/select-variable-reuse.sparql | 8 + .../sparql-1-1/select-variable-reuse.sparql | 3 + 10 files changed, 556 insertions(+) create mode 100644 packages/test-utils/statics/algebra/algebra-blank-to-var/sparql-1.1/aggregates/select-variable-reuse.json create mode 100644 packages/test-utils/statics/algebra/algebra/sparql-1.1/aggregates/select-variable-reuse.json create mode 100644 packages/test-utils/statics/algebra/canonical-sparql/base/sparql-1.1/aggregates/select-variable-reuse.sparql create mode 100644 packages/test-utils/statics/algebra/canonical-sparql/blank-to-var/sparql-1.1/aggregates/select-variable-reuse.sparql create mode 100644 packages/test-utils/statics/algebra/sparql/sparql-1.1/aggregates/select-variable-reuse.sparql create mode 100644 packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/select-variable-reuse.json create mode 100644 packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/select-variable-reuse.sparql create mode 100644 packages/test-utils/statics/ast/sparql-generated/sparql-1-1/select-variable-reuse.sparql create mode 100644 packages/test-utils/statics/ast/sparql/sparql-1-1/select-variable-reuse.sparql diff --git a/packages/rules-sparql-1-1/lib/validation/validators.ts b/packages/rules-sparql-1-1/lib/validation/validators.ts index 755d6a7d..1780929f 100644 --- a/packages/rules-sparql-1-1/lib/validation/validators.ts +++ b/packages/rules-sparql-1-1/lib/validation/validators.ts @@ -90,6 +90,8 @@ export function queryProjectionIsGood(query: Pick(); for (const selectVar of variables) { if (F.isTerm(selectVar)) { if (!groupBy || !groupBy.groupings.map(groupvar => getExpressionId(groupvar)) @@ -101,12 +103,18 @@ export function queryProjectionIsGood(query: Pick(); getVariablesFromExpression(selectVar.expression, usedvars); for (const usedvar of usedvars) { + if (asBoundVars.has(usedvar)) { + continue; + } if (!groupBy || !groupBy.groupings.map(groupVar => getExpressionId(groupVar)) .includes(usedvar)) { throw new Error(`Use of ungrouped variable in projection of operation (?${usedvar})`); } } } + if (!F.isTerm(selectVar)) { + asBoundVars.add(selectVar.variable.value); + } } } diff --git a/packages/test-utils/statics/algebra/algebra-blank-to-var/sparql-1.1/aggregates/select-variable-reuse.json b/packages/test-utils/statics/algebra/algebra-blank-to-var/sparql-1.1/aggregates/select-variable-reuse.json new file mode 100644 index 00000000..87c91b0c --- /dev/null +++ b/packages/test-utils/statics/algebra/algebra-blank-to-var/sparql-1.1/aggregates/select-variable-reuse.json @@ -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" + } + ] +} diff --git a/packages/test-utils/statics/algebra/algebra/sparql-1.1/aggregates/select-variable-reuse.json b/packages/test-utils/statics/algebra/algebra/sparql-1.1/aggregates/select-variable-reuse.json new file mode 100644 index 00000000..87c91b0c --- /dev/null +++ b/packages/test-utils/statics/algebra/algebra/sparql-1.1/aggregates/select-variable-reuse.json @@ -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" + } + ] +} diff --git a/packages/test-utils/statics/algebra/canonical-sparql/base/sparql-1.1/aggregates/select-variable-reuse.sparql b/packages/test-utils/statics/algebra/canonical-sparql/base/sparql-1.1/aggregates/select-variable-reuse.sparql new file mode 100644 index 00000000..86119c9f --- /dev/null +++ b/packages/test-utils/statics/algebra/canonical-sparql/base/sparql-1.1/aggregates/select-variable-reuse.sparql @@ -0,0 +1,8 @@ +SELECT ( COUNT( ?v ) AS ?count ) ( ( ?count + "1"^^ ) AS ?countPlusOne ) WHERE { + VALUES ?v { + "0"^^ + "1"^^ + "2"^^ + "3"^^ + } +} \ No newline at end of file diff --git a/packages/test-utils/statics/algebra/canonical-sparql/blank-to-var/sparql-1.1/aggregates/select-variable-reuse.sparql b/packages/test-utils/statics/algebra/canonical-sparql/blank-to-var/sparql-1.1/aggregates/select-variable-reuse.sparql new file mode 100644 index 00000000..86119c9f --- /dev/null +++ b/packages/test-utils/statics/algebra/canonical-sparql/blank-to-var/sparql-1.1/aggregates/select-variable-reuse.sparql @@ -0,0 +1,8 @@ +SELECT ( COUNT( ?v ) AS ?count ) ( ( ?count + "1"^^ ) AS ?countPlusOne ) WHERE { + VALUES ?v { + "0"^^ + "1"^^ + "2"^^ + "3"^^ + } +} \ No newline at end of file diff --git a/packages/test-utils/statics/algebra/sparql/sparql-1.1/aggregates/select-variable-reuse.sparql b/packages/test-utils/statics/algebra/sparql/sparql-1.1/aggregates/select-variable-reuse.sparql new file mode 100644 index 00000000..3709bb6e --- /dev/null +++ b/packages/test-utils/statics/algebra/sparql/sparql-1.1/aggregates/select-variable-reuse.sparql @@ -0,0 +1,3 @@ +SELECT (COUNT(?v) AS ?count) (?count + 1 AS ?countPlusOne) WHERE { + VALUES ?v { 0 1 2 3 } +} diff --git a/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/select-variable-reuse.json b/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/select-variable-reuse.json new file mode 100644 index 00000000..e064cbd2 --- /dev/null +++ b/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/select-variable-reuse.json @@ -0,0 +1,243 @@ +{ + "context": [], + "subType": "select", + "where": { + "type": "pattern", + "subType": "group", + "patterns": [ + { + "type": "pattern", + "subType": "values", + "variables": [ + { + "type": "term", + "subType": "variable", + "value": "v", + "loc": { + "sourceLocationType": "source", + "start": 76, + "end": 78 + } + } + ], + "values": [ + { + "v": { + "type": "term", + "subType": "literal", + "value": "0", + "langOrIri": { + "type": "term", + "subType": "namedNode", + "value": "http://www.w3.org/2001/XMLSchema#integer", + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "loc": { + "sourceLocationType": "source", + "start": 81, + "end": 82 + } + } + }, + { + "v": { + "type": "term", + "subType": "literal", + "value": "1", + "langOrIri": { + "type": "term", + "subType": "namedNode", + "value": "http://www.w3.org/2001/XMLSchema#integer", + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "loc": { + "sourceLocationType": "source", + "start": 83, + "end": 84 + } + } + }, + { + "v": { + "type": "term", + "subType": "literal", + "value": "2", + "langOrIri": { + "type": "term", + "subType": "namedNode", + "value": "http://www.w3.org/2001/XMLSchema#integer", + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "loc": { + "sourceLocationType": "source", + "start": 85, + "end": 86 + } + } + }, + { + "v": { + "type": "term", + "subType": "literal", + "value": "3", + "langOrIri": { + "type": "term", + "subType": "namedNode", + "value": "http://www.w3.org/2001/XMLSchema#integer", + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "loc": { + "sourceLocationType": "source", + "start": 87, + "end": 88 + } + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 69, + "end": 90 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 65, + "end": 92 + } + }, + "solutionModifiers": {}, + "datasets": { + "type": "datasetClauses", + "clauses": [], + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "variables": [ + { + "type": "pattern", + "subType": "bind", + "expression": { + "type": "expression", + "subType": "aggregate", + "aggregation": "count", + "distinct": false, + "loc": { + "sourceLocationType": "source", + "start": 8, + "end": 17 + }, + "expression": [ + { + "type": "term", + "subType": "variable", + "value": "v", + "loc": { + "sourceLocationType": "source", + "start": 14, + "end": 16 + } + } + ] + }, + "variable": { + "type": "term", + "subType": "variable", + "value": "count", + "loc": { + "sourceLocationType": "source", + "start": 21, + "end": 27 + } + }, + "loc": { + "sourceLocationType": "source", + "start": 7, + "end": 28 + } + }, + { + "type": "pattern", + "subType": "bind", + "expression": { + "type": "expression", + "subType": "operation", + "operator": "+", + "args": [ + { + "type": "term", + "subType": "variable", + "value": "count", + "loc": { + "sourceLocationType": "source", + "start": 30, + "end": 36 + } + }, + { + "type": "term", + "subType": "literal", + "value": "1", + "langOrIri": { + "type": "term", + "subType": "namedNode", + "value": "http://www.w3.org/2001/XMLSchema#integer", + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "loc": { + "sourceLocationType": "source", + "start": 39, + "end": 40 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 30, + "end": 40 + } + }, + "variable": { + "type": "term", + "subType": "variable", + "value": "countPlusOne", + "loc": { + "sourceLocationType": "source", + "start": 44, + "end": 57 + } + }, + "loc": { + "sourceLocationType": "source", + "start": 29, + "end": 58 + } + } + ], + "loc": { + "sourceLocationType": "inlinedSource", + "newSource": "SELECT (COUNT(?v) AS ?count) (?count + 1 AS ?countPlusOne) WHERE {\n VALUES ?v { 0 1 2 3 }\n}", + "start": 0, + "end": 9007199254740991, + "loc": { + "sourceLocationType": "source", + "start": 0, + "end": 92 + }, + "startOnNew": 0, + "endOnNew": 92 + }, + "type": "query" +} diff --git a/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/select-variable-reuse.sparql b/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/select-variable-reuse.sparql new file mode 100644 index 00000000..09a69889 --- /dev/null +++ b/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/select-variable-reuse.sparql @@ -0,0 +1 @@ +SELECT ( COUNT( ?v ) AS ?count ) ( ( ?count + "1"^^ ) AS ?countPlusOne ) WHERE { VALUES ?v { "0"^^ "1"^^ "2"^^ "3"^^ } } \ No newline at end of file diff --git a/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/select-variable-reuse.sparql b/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/select-variable-reuse.sparql new file mode 100644 index 00000000..86119c9f --- /dev/null +++ b/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/select-variable-reuse.sparql @@ -0,0 +1,8 @@ +SELECT ( COUNT( ?v ) AS ?count ) ( ( ?count + "1"^^ ) AS ?countPlusOne ) WHERE { + VALUES ?v { + "0"^^ + "1"^^ + "2"^^ + "3"^^ + } +} \ No newline at end of file diff --git a/packages/test-utils/statics/ast/sparql/sparql-1-1/select-variable-reuse.sparql b/packages/test-utils/statics/ast/sparql/sparql-1-1/select-variable-reuse.sparql new file mode 100644 index 00000000..adc1dd8b --- /dev/null +++ b/packages/test-utils/statics/ast/sparql/sparql-1-1/select-variable-reuse.sparql @@ -0,0 +1,3 @@ +SELECT (COUNT(?v) AS ?count) (?count + 1 AS ?countPlusOne) WHERE { + VALUES ?v { 0 1 2 3 } +} \ No newline at end of file From a800309fe1722257a51c24bcf43b13eaf39bd5f3 Mon Sep 17 00:00:00 2001 From: Jitse De Smet Date: Wed, 27 May 2026 09:04:01 +0000 Subject: [PATCH 2/6] some comments --- packages/rules-sparql-1-1/lib/validation/validators.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rules-sparql-1-1/lib/validation/validators.ts b/packages/rules-sparql-1-1/lib/validation/validators.ts index 1780929f..85b93607 100644 --- a/packages/rules-sparql-1-1/lib/validation/validators.ts +++ b/packages/rules-sparql-1-1/lib/validation/validators.ts @@ -103,6 +103,7 @@ export function queryProjectionIsGood(query: Pick(); getVariablesFromExpression(selectVar.expression, usedvars); for (const usedvar of usedvars) { + // If the var is created within the select, it is fine. if (asBoundVars.has(usedvar)) { continue; } @@ -113,6 +114,7 @@ export function queryProjectionIsGood(query: Pick Date: Wed, 3 Jun 2026 14:44:00 +0200 Subject: [PATCH 3/6] Split 1.1 and 1.2 validators --- engines/parser-sparql-1-2/lib/Parser.ts | 1 + packages/rules-sparql-1-1/lib/index.ts | 3 + .../lib/validation/validators.ts | 16 +--- packages/rules-sparql-1-2/lib/grammar.ts | 39 ++++++++- packages/rules-sparql-1-2/lib/index.ts | 12 +++ packages/rules-sparql-1-2/lib/validators.ts | 85 +++++++++++++++++++ .../select-variable-reuse.json | 0 .../select-variable-reuse.json | 0 .../select-variable-reuse.sparql | 0 .../select-variable-reuse.sparql | 0 .../select-variable-reuse.sparql | 0 .../select-variable-reuse.json | 0 .../select-variable-reuse.sparql | 0 .../select-variable-reuse.sparql | 0 .../select-variable-reuse.sparql | 0 15 files changed, 142 insertions(+), 14 deletions(-) rename packages/test-utils/statics/algebra/algebra-blank-to-var/{sparql-1.1/aggregates => sparql12}/select-variable-reuse.json (100%) rename packages/test-utils/statics/algebra/algebra/{sparql-1.1/aggregates => sparql12}/select-variable-reuse.json (100%) rename packages/test-utils/statics/algebra/canonical-sparql/base/{sparql-1.1/aggregates => sparql12}/select-variable-reuse.sparql (100%) rename packages/test-utils/statics/algebra/canonical-sparql/blank-to-var/{sparql-1.1/aggregates => sparql12}/select-variable-reuse.sparql (100%) rename packages/test-utils/statics/algebra/sparql/{sparql-1.1/aggregates => sparql12}/select-variable-reuse.sparql (100%) rename packages/test-utils/statics/ast/ast-source-tracked/{sparql-1-1 => sparql-1-2}/select-variable-reuse.json (100%) rename packages/test-utils/statics/ast/sparql-generated-compact/{sparql-1-1 => sparql-1-2}/select-variable-reuse.sparql (100%) rename packages/test-utils/statics/ast/sparql-generated/{sparql-1-1 => sparql-1-2}/select-variable-reuse.sparql (100%) rename packages/test-utils/statics/ast/sparql/{sparql-1-1 => sparql-1-2}/select-variable-reuse.sparql (100%) diff --git a/engines/parser-sparql-1-2/lib/Parser.ts b/engines/parser-sparql-1-2/lib/Parser.ts index eb811f83..12a0eab5 100644 --- a/engines/parser-sparql-1-2/lib/Parser.ts +++ b/engines/parser-sparql-1-2/lib/Parser.ts @@ -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) diff --git a/packages/rules-sparql-1-1/lib/index.ts b/packages/rules-sparql-1-1/lib/index.ts index 3e8286be..0578f5c4 100644 --- a/packages/rules-sparql-1-1/lib/index.ts +++ b/packages/rules-sparql-1-1/lib/index.ts @@ -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'; diff --git a/packages/rules-sparql-1-1/lib/validation/validators.ts b/packages/rules-sparql-1-1/lib/validation/validators.ts index 85b93607..f71b118a 100644 --- a/packages/rules-sparql-1-1/lib/validation/validators.ts +++ b/packages/rules-sparql-1-1/lib/validation/validators.ts @@ -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 ]; } @@ -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; @@ -53,7 +53,7 @@ function getExpressionId(expression: SolutionModifierGroupBind | Expression | Te /** * Get all variables used in an expression */ -function getVariablesFromExpression(expression: Expression, variables: Set): void { +export function getVariablesFromExpression(expression: Expression, variables: Set): void { if (F.isExpressionOperator(expression)) { for (const expr of expression.args) { getVariablesFromExpression(expr, variables); @@ -90,8 +90,6 @@ export function queryProjectionIsGood(query: Pick(); for (const selectVar of variables) { if (F.isTerm(selectVar)) { if (!groupBy || !groupBy.groupings.map(groupvar => getExpressionId(groupvar)) @@ -103,20 +101,12 @@ export function queryProjectionIsGood(query: Pick(); getVariablesFromExpression(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(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); - } } } diff --git a/packages/rules-sparql-1-2/lib/grammar.ts b/packages/rules-sparql-1-2/lib/grammar.ts index be6be180..ccac9e59 100644 --- a/packages/rules-sparql-1-2/lib/grammar.ts +++ b/packages/rules-sparql-1-2/lib/grammar.ts @@ -18,6 +18,7 @@ import type { GraphNode, GraphTerm, PatternBgp, + QuerySelect, Term, TermBlank, TermIri, @@ -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) @@ -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> = { + 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; + if (!C.skipValidation) { + queryProjectionIsGood(ret); + } + return ret; + }); + }, +}; + /** * [[8]](https://www.w3.org/TR/sparql12-query/#rVersionSpecifier) */ diff --git a/packages/rules-sparql-1-2/lib/index.ts b/packages/rules-sparql-1-2/lib/index.ts index 18807554..4f5b0ca4 100644 --- a/packages/rules-sparql-1-2/lib/index.ts +++ b/packages/rules-sparql-1-2/lib/index.ts @@ -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 = & typeof validation12> { + ...validation11, + ...validation12, +}; diff --git a/packages/rules-sparql-1-2/lib/validators.ts b/packages/rules-sparql-1-2/lib/validators.ts index 8c83c090..006a2d1b 100644 --- a/packages/rules-sparql-1-2/lib/validators.ts +++ b/packages/rules-sparql-1-2/lib/validators.ts @@ -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, @@ -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): 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 = > query.variables; + const hasCountAggregate = variables.flatMap( + varVal => F.isTerm(varVal) ? [] : getAggregatesOfExpression( 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(); + for (const selectVar of variables) { + if (F.isTerm(selectVar)) { + if (!groupBy || !groupBy.groupings.map(groupvar => + getExpressionId( groupvar)) + .includes((getExpressionId(selectVar)))) { + throw new Error('Variable not allowed in projection'); + } + } else if (getAggregatesOfExpression( selectVar.expression).length === 0) { + // Current value binding does not use aggregates + const usedvars = new Set(); + getVariablesFromExpression( 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(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(); + for (const variable of variables) { + if ('variable' in variable) { + selectBoundedVars.add(variable.variable.value); + } + } + + // Look at in scope variables + const vars = subqueries.flatMap(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`); + } + } + } +} diff --git a/packages/test-utils/statics/algebra/algebra-blank-to-var/sparql-1.1/aggregates/select-variable-reuse.json b/packages/test-utils/statics/algebra/algebra-blank-to-var/sparql12/select-variable-reuse.json similarity index 100% rename from packages/test-utils/statics/algebra/algebra-blank-to-var/sparql-1.1/aggregates/select-variable-reuse.json rename to packages/test-utils/statics/algebra/algebra-blank-to-var/sparql12/select-variable-reuse.json diff --git a/packages/test-utils/statics/algebra/algebra/sparql-1.1/aggregates/select-variable-reuse.json b/packages/test-utils/statics/algebra/algebra/sparql12/select-variable-reuse.json similarity index 100% rename from packages/test-utils/statics/algebra/algebra/sparql-1.1/aggregates/select-variable-reuse.json rename to packages/test-utils/statics/algebra/algebra/sparql12/select-variable-reuse.json diff --git a/packages/test-utils/statics/algebra/canonical-sparql/base/sparql-1.1/aggregates/select-variable-reuse.sparql b/packages/test-utils/statics/algebra/canonical-sparql/base/sparql12/select-variable-reuse.sparql similarity index 100% rename from packages/test-utils/statics/algebra/canonical-sparql/base/sparql-1.1/aggregates/select-variable-reuse.sparql rename to packages/test-utils/statics/algebra/canonical-sparql/base/sparql12/select-variable-reuse.sparql diff --git a/packages/test-utils/statics/algebra/canonical-sparql/blank-to-var/sparql-1.1/aggregates/select-variable-reuse.sparql b/packages/test-utils/statics/algebra/canonical-sparql/blank-to-var/sparql12/select-variable-reuse.sparql similarity index 100% rename from packages/test-utils/statics/algebra/canonical-sparql/blank-to-var/sparql-1.1/aggregates/select-variable-reuse.sparql rename to packages/test-utils/statics/algebra/canonical-sparql/blank-to-var/sparql12/select-variable-reuse.sparql diff --git a/packages/test-utils/statics/algebra/sparql/sparql-1.1/aggregates/select-variable-reuse.sparql b/packages/test-utils/statics/algebra/sparql/sparql12/select-variable-reuse.sparql similarity index 100% rename from packages/test-utils/statics/algebra/sparql/sparql-1.1/aggregates/select-variable-reuse.sparql rename to packages/test-utils/statics/algebra/sparql/sparql12/select-variable-reuse.sparql diff --git a/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/select-variable-reuse.json b/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-2/select-variable-reuse.json similarity index 100% rename from packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/select-variable-reuse.json rename to packages/test-utils/statics/ast/ast-source-tracked/sparql-1-2/select-variable-reuse.json diff --git a/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/select-variable-reuse.sparql b/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-2/select-variable-reuse.sparql similarity index 100% rename from packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/select-variable-reuse.sparql rename to packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-2/select-variable-reuse.sparql diff --git a/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/select-variable-reuse.sparql b/packages/test-utils/statics/ast/sparql-generated/sparql-1-2/select-variable-reuse.sparql similarity index 100% rename from packages/test-utils/statics/ast/sparql-generated/sparql-1-1/select-variable-reuse.sparql rename to packages/test-utils/statics/ast/sparql-generated/sparql-1-2/select-variable-reuse.sparql diff --git a/packages/test-utils/statics/ast/sparql/sparql-1-1/select-variable-reuse.sparql b/packages/test-utils/statics/ast/sparql/sparql-1-2/select-variable-reuse.sparql similarity index 100% rename from packages/test-utils/statics/ast/sparql/sparql-1-1/select-variable-reuse.sparql rename to packages/test-utils/statics/ast/sparql/sparql-1-2/select-variable-reuse.sparql From 2874295dbd6cab30a212aaa1835f11ca1b4cdb58 Mon Sep 17 00:00:00 2001 From: Jitse De Smet Date: Wed, 3 Jun 2026 13:06:22 +0000 Subject: [PATCH 4/6] test: achieve 100% coverage on rules-sparql-1-2 validators The queryProjectionIsGood function in rules-sparql-1-2/validators.ts is a separate implementation from the 1.1 version (changed per w3c/sparql-query#380). Although all 1.1 tests correctly run against the 1.2 parser, several branches of this function were not reached by any existing test. Fix by adding cases to the shared test infrastructure so they run on both parsers automatically: - 3 new sparql-1-1-invalid static queries (select-star-with-group-by, ungrouped-variable-in-expression-projection, count-with-ungrouped-expression-variable) that cover the throw paths in queryProjectionIsGood (lines 123 and 158). - 3 new note tests in importSparql11NoteTests covering the no-throw branches: AS variable not conflicting with a term-var subquery, AS variable not conflicting with a wildcard subquery, and expression projection variable covered by GROUP BY (line 158 and 183-186 FALSE branches). Also add rapport.md documenting the investigation findings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/test-utils/lib/Sparql11NotesTest.ts | 12 +++ ...-with-ungrouped-expression-variable.sparql | 1 + .../select-star-with-group-by.sparql | 1 + ...d-variable-in-expression-projection.sparql | 1 + rapport.md | 74 +++++++++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/count-with-ungrouped-expression-variable.sparql create mode 100644 packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/select-star-with-group-by.sparql create mode 100644 packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/ungrouped-variable-in-expression-projection.sparql create mode 100644 rapport.md diff --git a/packages/test-utils/lib/Sparql11NotesTest.ts b/packages/test-utils/lib/Sparql11NotesTest.ts index c999d61b..5b781a42 100644 --- a/packages/test-utils/lib/Sparql11NotesTest.ts +++ b/packages/test-utils/lib/Sparql11NotesTest.ts @@ -55,6 +55,18 @@ export function importSparql11NoteTests(parser: Parser, _DF: DataFactory { + expect(parser.parse('SELECT (1 AS ?x) WHERE { SELECT ?y WHERE { ?y ?p ?o } }')).toMatchObject({}); + }); + + it('should NOT throw when AS variable does not appear in subquery with wildcard projection', ({ expect }) => { + expect(parser.parse('SELECT (1 AS ?x) WHERE { SELECT * WHERE { ?s ?p ?o } }')).toMatchObject({}); + }); + + it('should NOT throw when expression projection variable is covered by GROUP BY', ({ expect }) => { + expect(parser.parse('SELECT (?x + 1 AS ?result) WHERE { ?s ?p ?x } GROUP BY ?x')).toMatchObject({}); + }); + it('should throw an error on bind to variable in scope', testErroneousQuery( 'SELECT * { ?s ?p ?o BIND(?o AS ?o) }', 'Target id of \'AS\' (?X) already used in subquery', diff --git a/packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/count-with-ungrouped-expression-variable.sparql b/packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/count-with-ungrouped-expression-variable.sparql new file mode 100644 index 00000000..76ab54cd --- /dev/null +++ b/packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/count-with-ungrouped-expression-variable.sparql @@ -0,0 +1 @@ +SELECT (COUNT(?y) AS ?cnt) (?y + 1 AS ?result) WHERE { ?s ?p ?y } diff --git a/packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/select-star-with-group-by.sparql b/packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/select-star-with-group-by.sparql new file mode 100644 index 00000000..bc66d86d --- /dev/null +++ b/packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/select-star-with-group-by.sparql @@ -0,0 +1 @@ +SELECT * WHERE { ?s ?p ?o } GROUP BY ?s diff --git a/packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/ungrouped-variable-in-expression-projection.sparql b/packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/ungrouped-variable-in-expression-projection.sparql new file mode 100644 index 00000000..a14bb8a7 --- /dev/null +++ b/packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/ungrouped-variable-in-expression-projection.sparql @@ -0,0 +1 @@ +SELECT (?y + 1 AS ?result) WHERE { ?s ?p ?y } GROUP BY ?s diff --git a/rapport.md b/rapport.md new file mode 100644 index 00000000..8d8b3290 --- /dev/null +++ b/rapport.md @@ -0,0 +1,74 @@ +# Rapport: SPARQL 1.1 Tests on the SPARQL 1.2 Parser + +## Finding: 1.1 Tests ARE Run on the 1.2 Parser + +Yes, all SPARQL 1.1 test suites are executed against the 1.2 parser. In +`engines/parser-sparql-1-2/test/statics.test.ts`, the following test groups +explicitly run 1.1 content against the 1.2 parser: + +- **`describe('positive paths')`** — runs all path tests from the `paths` suite. +- **`describe('positive sparql 1.1')`** — runs every positive 1.1 query, comparing + output while ignoring the new `annotations` field (valid because 1.2 adds + annotation syntax to triples, absent in pure 1.1 queries). +- **`describe('negative SPARQL 1.1')`** — runs every negative 1.1 test and + asserts that the 1.2 parser also rejects them. +- **`describe('specific sparql 1.1 with/without source tracking')`** — + `importSparql11NoteTests` is called with the 1.2 parser, running the entire + W3C SPARQL 1.1 note test suite against it. + +### Negative Tests Filter + +No negative 1.1 tests are filtered out for the 1.2 parser: all `sparql-1-1-invalid` +queries are expected to remain invalid under SPARQL 1.2. A filter *is* applied to +the **1.2-specific** negative tests (two tests that are no longer invalid in 1.2 +are skipped via a `skip` set). + +## Coverage Gap Found + +Although the 1.1 tests run correctly on the 1.2 parser, code coverage was below +100% because `packages/rules-sparql-1-2/lib/validators.ts` defines its **own** +`queryProjectionIsGood` function (required because SPARQL 1.2 changed validation +rule §9 per [w3c/sparql-query#380](https://github.com/w3c/sparql-query/pull/380)). +This function is structurally similar to the 1.1 version, but it is a separate +code path, and the existing shared tests did not exercise all of its branches. + +The uncovered branches were: + +| Lines | Branch | What was missing | +|-------|--------|-----------------| +| 123 | `throw new Error('GROUP BY not allowed with wildcard')` | `SELECT *` with `GROUP BY` not in the test suite | +| 158 | `!groupBy` TRUE (short-circuit) | COUNT aggregate without explicit GROUP BY, expression binding uses ungrouped variable | +| 158 FALSE | condition `!groupBy \|\| !...includes(usedvar)` is FALSE | Expression binding's variable IS in GROUP BY (no throw) — not in test suite | +| 183 | `F.isWildcard(v) ? '*' : …` | Subquery projecting a wildcard (`SELECT *`) — not in test suite | +| 186 FALSE | `subqueryIds.has(selectedVarId)` is FALSE | AS-bound variable not conflicting with subquery — not in test suite | + +## Fix — All via Shared Test Infrastructure + +Since the 1.1 tests already run on the 1.2 parser, the right fix is to add the +missing cases to the shared test infrastructure rather than as isolated unit tests. +No changes were made to `rules-sparql-1-2/test/validators.test.ts`. + +### Three new `sparql-1-1-invalid` static queries + +Added to `packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/`. These are +automatically exercised against **both** the 1.1 and 1.2 parsers: + +| File | Covers | +|------|--------| +| `select-star-with-group-by.sparql` | line 123 throw | +| `ungrouped-variable-in-expression-projection.sparql` | line 158 throw (groupBy exists) | +| `count-with-ungrouped-expression-variable.sparql` | line 158 `!groupBy` TRUE throw | + +### Three new note tests in `importSparql11NoteTests` + +Added to `packages/test-utils/lib/Sparql11NotesTest.ts`. These are automatically +run against **both** the 1.1 and 1.2 parsers: + +| Test | Covers | +|------|--------| +| AS variable not in subquery (term var projection) | line 183 branch A, line 186 FALSE | +| AS variable not in subquery (wildcard projection) | line 183 branch B, line 186 FALSE | +| Expression projection variable covered by GROUP BY | line 158 FALSE (no throw) | + +After the fix, all coverage thresholds (lines, statements, branches, functions) +pass at 100%. From 54c32d4933e3f8ec84bab1098301bfbaaede7a43 Mon Sep 17 00:00:00 2001 From: Jitse De Smet Date: Wed, 3 Jun 2026 14:16:29 +0000 Subject: [PATCH 5/6] test: move 3 note tests from Sparql11NotesTest to static files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the three programmatic 'should NOT throw' tests added in the previous commit with proper static test files that slot into the shared positive-test infrastructure and therefore run automatically on all parsers (1.1, 1.1-adjust, 1.2) and the 1.2 generator. Files added per test case: - sparql/sparql-1-1/.sparql – the query under test - ast-source-tracked/sparql-1-1/.json – expected AST (source-tracked) - sparql-generated/sparql-1-1/.sparql – expected generator output - sparql-generated-compact/sparql-1-1/.sparql – compact generator output Cases migrated: - note-as-var-not-in-term-subquery - note-as-var-not-in-wildcard-subquery - note-expression-grouped-var Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/test-utils/lib/Sparql11NotesTest.ts | 12 -- .../note-as-var-not-in-term-subquery.json | 166 +++++++++++++++++ .../note-as-var-not-in-wildcard-subquery.json | 164 +++++++++++++++++ .../note-expression-grouped-var.json | 169 ++++++++++++++++++ .../note-as-var-not-in-term-subquery.sparql | 1 + ...ote-as-var-not-in-wildcard-subquery.sparql | 1 + .../note-expression-grouped-var.sparql | 1 + .../note-as-var-not-in-term-subquery.sparql | 5 + ...ote-as-var-not-in-wildcard-subquery.sparql | 5 + .../note-expression-grouped-var.sparql | 4 + .../note-as-var-not-in-term-subquery.sparql | 1 + ...ote-as-var-not-in-wildcard-subquery.sparql | 1 + .../note-expression-grouped-var.sparql | 1 + 13 files changed, 519 insertions(+), 12 deletions(-) create mode 100644 packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-as-var-not-in-term-subquery.json create mode 100644 packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-as-var-not-in-wildcard-subquery.json create mode 100644 packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-expression-grouped-var.json create mode 100644 packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-as-var-not-in-term-subquery.sparql create mode 100644 packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql create mode 100644 packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-expression-grouped-var.sparql create mode 100644 packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-as-var-not-in-term-subquery.sparql create mode 100644 packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql create mode 100644 packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-expression-grouped-var.sparql create mode 100644 packages/test-utils/statics/ast/sparql/sparql-1-1/note-as-var-not-in-term-subquery.sparql create mode 100644 packages/test-utils/statics/ast/sparql/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql create mode 100644 packages/test-utils/statics/ast/sparql/sparql-1-1/note-expression-grouped-var.sparql diff --git a/packages/test-utils/lib/Sparql11NotesTest.ts b/packages/test-utils/lib/Sparql11NotesTest.ts index 5b781a42..c999d61b 100644 --- a/packages/test-utils/lib/Sparql11NotesTest.ts +++ b/packages/test-utils/lib/Sparql11NotesTest.ts @@ -55,18 +55,6 @@ export function importSparql11NoteTests(parser: Parser, _DF: DataFactory { - expect(parser.parse('SELECT (1 AS ?x) WHERE { SELECT ?y WHERE { ?y ?p ?o } }')).toMatchObject({}); - }); - - it('should NOT throw when AS variable does not appear in subquery with wildcard projection', ({ expect }) => { - expect(parser.parse('SELECT (1 AS ?x) WHERE { SELECT * WHERE { ?s ?p ?o } }')).toMatchObject({}); - }); - - it('should NOT throw when expression projection variable is covered by GROUP BY', ({ expect }) => { - expect(parser.parse('SELECT (?x + 1 AS ?result) WHERE { ?s ?p ?x } GROUP BY ?x')).toMatchObject({}); - }); - it('should throw an error on bind to variable in scope', testErroneousQuery( 'SELECT * { ?s ?p ?o BIND(?o AS ?o) }', 'Target id of \'AS\' (?X) already used in subquery', diff --git a/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-as-var-not-in-term-subquery.json b/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-as-var-not-in-term-subquery.json new file mode 100644 index 00000000..002af35f --- /dev/null +++ b/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-as-var-not-in-term-subquery.json @@ -0,0 +1,166 @@ +{ + "context": [], + "subType": "select", + "where": { + "type": "pattern", + "subType": "group", + "patterns": [ + { + "type": "query", + "subType": "select", + "where": { + "type": "pattern", + "subType": "group", + "patterns": [ + { + "type": "pattern", + "subType": "bgp", + "triples": [ + { + "type": "triple", + "subject": { + "type": "term", + "subType": "variable", + "value": "y", + "loc": { + "sourceLocationType": "source", + "start": 43, + "end": 45 + } + }, + "predicate": { + "type": "term", + "subType": "variable", + "value": "p", + "loc": { + "sourceLocationType": "source", + "start": 46, + "end": 48 + } + }, + "object": { + "type": "term", + "subType": "variable", + "value": "o", + "loc": { + "sourceLocationType": "source", + "start": 49, + "end": 51 + } + }, + "loc": { + "sourceLocationType": "source", + "start": 43, + "end": 51 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 43, + "end": 51 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 41, + "end": 53 + } + }, + "datasets": { + "type": "datasetClauses", + "clauses": [], + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "context": [], + "solutionModifiers": {}, + "variables": [ + { + "type": "term", + "subType": "variable", + "value": "y", + "loc": { + "sourceLocationType": "source", + "start": 32, + "end": 34 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 25, + "end": 53 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 23, + "end": 55 + } + }, + "solutionModifiers": {}, + "datasets": { + "type": "datasetClauses", + "clauses": [], + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "variables": [ + { + "type": "pattern", + "subType": "bind", + "expression": { + "type": "term", + "subType": "literal", + "value": "1", + "langOrIri": { + "type": "term", + "subType": "namedNode", + "value": "http://www.w3.org/2001/XMLSchema#integer", + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "loc": { + "sourceLocationType": "source", + "start": 8, + "end": 9 + } + }, + "variable": { + "type": "term", + "subType": "variable", + "value": "x", + "loc": { + "sourceLocationType": "source", + "start": 13, + "end": 15 + } + }, + "loc": { + "sourceLocationType": "source", + "start": 7, + "end": 16 + } + } + ], + "loc": { + "sourceLocationType": "inlinedSource", + "newSource": "SELECT (1 AS ?x) WHERE { SELECT ?y WHERE { ?y ?p ?o } }\n", + "start": 0, + "end": 9007199254740991, + "loc": { + "sourceLocationType": "source", + "start": 0, + "end": 55 + }, + "startOnNew": 0, + "endOnNew": 55 + }, + "type": "query" +} diff --git a/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-as-var-not-in-wildcard-subquery.json b/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-as-var-not-in-wildcard-subquery.json new file mode 100644 index 00000000..8412d147 --- /dev/null +++ b/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-as-var-not-in-wildcard-subquery.json @@ -0,0 +1,164 @@ +{ + "context": [], + "subType": "select", + "where": { + "type": "pattern", + "subType": "group", + "patterns": [ + { + "type": "query", + "subType": "select", + "where": { + "type": "pattern", + "subType": "group", + "patterns": [ + { + "type": "pattern", + "subType": "bgp", + "triples": [ + { + "type": "triple", + "subject": { + "type": "term", + "subType": "variable", + "value": "s", + "loc": { + "sourceLocationType": "source", + "start": 42, + "end": 44 + } + }, + "predicate": { + "type": "term", + "subType": "variable", + "value": "p", + "loc": { + "sourceLocationType": "source", + "start": 45, + "end": 47 + } + }, + "object": { + "type": "term", + "subType": "variable", + "value": "o", + "loc": { + "sourceLocationType": "source", + "start": 48, + "end": 50 + } + }, + "loc": { + "sourceLocationType": "source", + "start": 42, + "end": 50 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 42, + "end": 50 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 40, + "end": 52 + } + }, + "datasets": { + "type": "datasetClauses", + "clauses": [], + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "context": [], + "solutionModifiers": {}, + "variables": [ + { + "type": "wildcard", + "loc": { + "sourceLocationType": "source", + "start": 32, + "end": 33 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 25, + "end": 52 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 23, + "end": 54 + } + }, + "solutionModifiers": {}, + "datasets": { + "type": "datasetClauses", + "clauses": [], + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "variables": [ + { + "type": "pattern", + "subType": "bind", + "expression": { + "type": "term", + "subType": "literal", + "value": "1", + "langOrIri": { + "type": "term", + "subType": "namedNode", + "value": "http://www.w3.org/2001/XMLSchema#integer", + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "loc": { + "sourceLocationType": "source", + "start": 8, + "end": 9 + } + }, + "variable": { + "type": "term", + "subType": "variable", + "value": "x", + "loc": { + "sourceLocationType": "source", + "start": 13, + "end": 15 + } + }, + "loc": { + "sourceLocationType": "source", + "start": 7, + "end": 16 + } + } + ], + "loc": { + "sourceLocationType": "inlinedSource", + "newSource": "SELECT (1 AS ?x) WHERE { SELECT * WHERE { ?s ?p ?o } }\n", + "start": 0, + "end": 9007199254740991, + "loc": { + "sourceLocationType": "source", + "start": 0, + "end": 54 + }, + "startOnNew": 0, + "endOnNew": 54 + }, + "type": "query" +} diff --git a/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-expression-grouped-var.json b/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-expression-grouped-var.json new file mode 100644 index 00000000..f4bd10db --- /dev/null +++ b/packages/test-utils/statics/ast/ast-source-tracked/sparql-1-1/note-expression-grouped-var.json @@ -0,0 +1,169 @@ +{ + "context": [], + "subType": "select", + "where": { + "type": "pattern", + "subType": "group", + "patterns": [ + { + "type": "pattern", + "subType": "bgp", + "triples": [ + { + "type": "triple", + "subject": { + "type": "term", + "subType": "variable", + "value": "s", + "loc": { + "sourceLocationType": "source", + "start": 35, + "end": 37 + } + }, + "predicate": { + "type": "term", + "subType": "variable", + "value": "p", + "loc": { + "sourceLocationType": "source", + "start": 38, + "end": 40 + } + }, + "object": { + "type": "term", + "subType": "variable", + "value": "x", + "loc": { + "sourceLocationType": "source", + "start": 41, + "end": 43 + } + }, + "loc": { + "sourceLocationType": "source", + "start": 35, + "end": 43 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 35, + "end": 43 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 33, + "end": 45 + } + }, + "solutionModifiers": { + "group": { + "type": "solutionModifier", + "subType": "group", + "groupings": [ + { + "type": "term", + "subType": "variable", + "value": "x", + "loc": { + "sourceLocationType": "source", + "start": 55, + "end": 57 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 46, + "end": 57 + } + } + }, + "datasets": { + "type": "datasetClauses", + "clauses": [], + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "variables": [ + { + "type": "pattern", + "subType": "bind", + "expression": { + "type": "expression", + "subType": "operation", + "operator": "+", + "args": [ + { + "type": "term", + "subType": "variable", + "value": "x", + "loc": { + "sourceLocationType": "source", + "start": 8, + "end": 10 + } + }, + { + "type": "term", + "subType": "literal", + "value": "1", + "langOrIri": { + "type": "term", + "subType": "namedNode", + "value": "http://www.w3.org/2001/XMLSchema#integer", + "loc": { + "sourceLocationType": "noMaterialize" + } + }, + "loc": { + "sourceLocationType": "source", + "start": 13, + "end": 14 + } + } + ], + "loc": { + "sourceLocationType": "source", + "start": 8, + "end": 14 + } + }, + "variable": { + "type": "term", + "subType": "variable", + "value": "result", + "loc": { + "sourceLocationType": "source", + "start": 18, + "end": 25 + } + }, + "loc": { + "sourceLocationType": "source", + "start": 7, + "end": 26 + } + } + ], + "loc": { + "sourceLocationType": "inlinedSource", + "newSource": "SELECT (?x + 1 AS ?result) WHERE { ?s ?p ?x } GROUP BY ?x\n", + "start": 0, + "end": 9007199254740991, + "loc": { + "sourceLocationType": "source", + "start": 0, + "end": 57 + }, + "startOnNew": 0, + "endOnNew": 57 + }, + "type": "query" +} diff --git a/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-as-var-not-in-term-subquery.sparql b/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-as-var-not-in-term-subquery.sparql new file mode 100644 index 00000000..7eff8cd7 --- /dev/null +++ b/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-as-var-not-in-term-subquery.sparql @@ -0,0 +1 @@ +SELECT ( "1"^^ AS ?x ) WHERE { SELECT ?y WHERE { ?y ?p ?o . } } \ No newline at end of file diff --git a/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql b/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql new file mode 100644 index 00000000..1b9e77d8 --- /dev/null +++ b/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql @@ -0,0 +1 @@ +SELECT ( "1"^^ AS ?x ) WHERE { SELECT * WHERE { ?s ?p ?o . } } \ No newline at end of file diff --git a/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-expression-grouped-var.sparql b/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-expression-grouped-var.sparql new file mode 100644 index 00000000..4c74614c --- /dev/null +++ b/packages/test-utils/statics/ast/sparql-generated-compact/sparql-1-1/note-expression-grouped-var.sparql @@ -0,0 +1 @@ +SELECT ( ( ?x + "1"^^ ) AS ?result ) WHERE { ?s ?p ?x . } GROUP BY ?x \ No newline at end of file diff --git a/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-as-var-not-in-term-subquery.sparql b/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-as-var-not-in-term-subquery.sparql new file mode 100644 index 00000000..605a6816 --- /dev/null +++ b/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-as-var-not-in-term-subquery.sparql @@ -0,0 +1,5 @@ +SELECT ( "1"^^ AS ?x ) WHERE { + SELECT ?y WHERE { + ?y ?p ?o . + } +} \ No newline at end of file diff --git a/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql b/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql new file mode 100644 index 00000000..1820194c --- /dev/null +++ b/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql @@ -0,0 +1,5 @@ +SELECT ( "1"^^ AS ?x ) WHERE { + SELECT * WHERE { + ?s ?p ?o . + } +} \ No newline at end of file diff --git a/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-expression-grouped-var.sparql b/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-expression-grouped-var.sparql new file mode 100644 index 00000000..e6a3ab5e --- /dev/null +++ b/packages/test-utils/statics/ast/sparql-generated/sparql-1-1/note-expression-grouped-var.sparql @@ -0,0 +1,4 @@ +SELECT ( ( ?x + "1"^^ ) AS ?result ) WHERE { + ?s ?p ?x . +} +GROUP BY ?x \ No newline at end of file diff --git a/packages/test-utils/statics/ast/sparql/sparql-1-1/note-as-var-not-in-term-subquery.sparql b/packages/test-utils/statics/ast/sparql/sparql-1-1/note-as-var-not-in-term-subquery.sparql new file mode 100644 index 00000000..d3c69a00 --- /dev/null +++ b/packages/test-utils/statics/ast/sparql/sparql-1-1/note-as-var-not-in-term-subquery.sparql @@ -0,0 +1 @@ +SELECT (1 AS ?x) WHERE { SELECT ?y WHERE { ?y ?p ?o } } diff --git a/packages/test-utils/statics/ast/sparql/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql b/packages/test-utils/statics/ast/sparql/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql new file mode 100644 index 00000000..6dd1a8ce --- /dev/null +++ b/packages/test-utils/statics/ast/sparql/sparql-1-1/note-as-var-not-in-wildcard-subquery.sparql @@ -0,0 +1 @@ +SELECT (1 AS ?x) WHERE { SELECT * WHERE { ?s ?p ?o } } diff --git a/packages/test-utils/statics/ast/sparql/sparql-1-1/note-expression-grouped-var.sparql b/packages/test-utils/statics/ast/sparql/sparql-1-1/note-expression-grouped-var.sparql new file mode 100644 index 00000000..54aaf3dc --- /dev/null +++ b/packages/test-utils/statics/ast/sparql/sparql-1-1/note-expression-grouped-var.sparql @@ -0,0 +1 @@ +SELECT (?x + 1 AS ?result) WHERE { ?s ?p ?x } GROUP BY ?x From 394418e328a3b7bff60fae468e713875fcbacb7f Mon Sep 17 00:00:00 2001 From: Jitse De Smet Date: Wed, 3 Jun 2026 14:22:13 +0000 Subject: [PATCH 6/6] No rapport --- rapport.md | 74 ------------------------------------------------------ 1 file changed, 74 deletions(-) delete mode 100644 rapport.md diff --git a/rapport.md b/rapport.md deleted file mode 100644 index 8d8b3290..00000000 --- a/rapport.md +++ /dev/null @@ -1,74 +0,0 @@ -# Rapport: SPARQL 1.1 Tests on the SPARQL 1.2 Parser - -## Finding: 1.1 Tests ARE Run on the 1.2 Parser - -Yes, all SPARQL 1.1 test suites are executed against the 1.2 parser. In -`engines/parser-sparql-1-2/test/statics.test.ts`, the following test groups -explicitly run 1.1 content against the 1.2 parser: - -- **`describe('positive paths')`** — runs all path tests from the `paths` suite. -- **`describe('positive sparql 1.1')`** — runs every positive 1.1 query, comparing - output while ignoring the new `annotations` field (valid because 1.2 adds - annotation syntax to triples, absent in pure 1.1 queries). -- **`describe('negative SPARQL 1.1')`** — runs every negative 1.1 test and - asserts that the 1.2 parser also rejects them. -- **`describe('specific sparql 1.1 with/without source tracking')`** — - `importSparql11NoteTests` is called with the 1.2 parser, running the entire - W3C SPARQL 1.1 note test suite against it. - -### Negative Tests Filter - -No negative 1.1 tests are filtered out for the 1.2 parser: all `sparql-1-1-invalid` -queries are expected to remain invalid under SPARQL 1.2. A filter *is* applied to -the **1.2-specific** negative tests (two tests that are no longer invalid in 1.2 -are skipped via a `skip` set). - -## Coverage Gap Found - -Although the 1.1 tests run correctly on the 1.2 parser, code coverage was below -100% because `packages/rules-sparql-1-2/lib/validators.ts` defines its **own** -`queryProjectionIsGood` function (required because SPARQL 1.2 changed validation -rule §9 per [w3c/sparql-query#380](https://github.com/w3c/sparql-query/pull/380)). -This function is structurally similar to the 1.1 version, but it is a separate -code path, and the existing shared tests did not exercise all of its branches. - -The uncovered branches were: - -| Lines | Branch | What was missing | -|-------|--------|-----------------| -| 123 | `throw new Error('GROUP BY not allowed with wildcard')` | `SELECT *` with `GROUP BY` not in the test suite | -| 158 | `!groupBy` TRUE (short-circuit) | COUNT aggregate without explicit GROUP BY, expression binding uses ungrouped variable | -| 158 FALSE | condition `!groupBy \|\| !...includes(usedvar)` is FALSE | Expression binding's variable IS in GROUP BY (no throw) — not in test suite | -| 183 | `F.isWildcard(v) ? '*' : …` | Subquery projecting a wildcard (`SELECT *`) — not in test suite | -| 186 FALSE | `subqueryIds.has(selectedVarId)` is FALSE | AS-bound variable not conflicting with subquery — not in test suite | - -## Fix — All via Shared Test Infrastructure - -Since the 1.1 tests already run on the 1.2 parser, the right fix is to add the -missing cases to the shared test infrastructure rather than as isolated unit tests. -No changes were made to `rules-sparql-1-2/test/validators.test.ts`. - -### Three new `sparql-1-1-invalid` static queries - -Added to `packages/test-utils/statics/ast/sparql/sparql-1-1-invalid/`. These are -automatically exercised against **both** the 1.1 and 1.2 parsers: - -| File | Covers | -|------|--------| -| `select-star-with-group-by.sparql` | line 123 throw | -| `ungrouped-variable-in-expression-projection.sparql` | line 158 throw (groupBy exists) | -| `count-with-ungrouped-expression-variable.sparql` | line 158 `!groupBy` TRUE throw | - -### Three new note tests in `importSparql11NoteTests` - -Added to `packages/test-utils/lib/Sparql11NotesTest.ts`. These are automatically -run against **both** the 1.1 and 1.2 parsers: - -| Test | Covers | -|------|--------| -| AS variable not in subquery (term var projection) | line 183 branch A, line 186 FALSE | -| AS variable not in subquery (wildcard projection) | line 183 branch B, line 186 FALSE | -| Expression projection variable covered by GROUP BY | line 158 FALSE (no throw) | - -After the fix, all coverage thresholds (lines, statements, branches, functions) -pass at 100%.