feat: scalar expressions in JSON-LD select (parity with SPARQL)#1227
Conversation
zonotope
left a comment
There was a problem hiding this comment.
looks good besides two small things.
| /// `Column::Var(alias)` projection plus a `Pattern::Bind { var: alias, expr }` | ||
| /// injected into the patterns list — or into `options.post_binds` when | ||
| /// the expression references an aggregate output variable. | ||
| Expr { |
There was a problem hiding this comment.
Not a big deal if it's hard to change, but naming the type the same thing as one of its fields can be confusing. Perhaps Computation would be better for the variant name, or code for the field name would disambiguate things.
| for column in columns { | ||
| if let UnresolvedColumn::Expr { expr, alias } = column { | ||
| // Subqueries do not currently expose post-aggregation binds, so | ||
| // disallow expressions that would need to run after aggregation. | ||
| // This mirrors the limitation of `SubqueryPattern` (no `post_binds`). | ||
| if aggregate_output_vars.contains(alias) | ||
| || expression_references_any(expr, &aggregate_output_vars) | ||
| { | ||
| return Err(ParseError::InvalidSelect(format!( | ||
| "select expression '{alias}' references an aggregate output; \ | ||
| post-aggregation BINDs are not supported inside subqueries" | ||
| ))); | ||
| } | ||
| let alias_var = vars.get_or_insert(alias); | ||
| let lowered_expr = lower_filter_expr_with_encoder(expr, vars, encoder, pp_counter)?; | ||
| patterns.push(Pattern::Bind { | ||
| var: alias_var, | ||
| expr: lowered_expr, | ||
| }); | ||
| } |
There was a problem hiding this comment.
this structure is very close the one for lower_query. I think you could have a helper that processes one column at a time returning a pre or post bind (you'd also have to define an enum so you can differentiate the two cases).
Then, the lower_query loop calls that helper and accumulates both lists, and this lower_subquery loop also calls the same helper, but returns an error if the helper ever returns a post-bind.
That seems like a lot of ceremony for two private functions, but it will make the behavior easier to change in the future, and make any bugs that might exist in the logic easier to fix.
SELECT (expr AS ?alias)is now expressible in JSON-LD queries via(as <expr> ?alias). The two languages share the same query IR, so this ispurely a JSON-LD parse/lower change — no SPARQL or executor changes.
{ "select": ["?p", "(as (coalesce ?titleFr ?titleEn \"untitled\") ?title)"] }Previously rejected with
Unknown aggregate function: coalesce, because theJSON-LD select clause assumed every S-expression item was an aggregate. Now:
(<aggregate> ...)— existing auto-aliased aggregate(as (<aggregate> ...) ?alias)— existing aliased aggregate(as <expr> ?alias)— new: scalar expression desugared toPattern::BindA bare scalar without an alias errors with a clear pointer to the
(as ...)form.Function parity
Every SPARQL
FunctionNamevariant has a corresponding name in JSON-LD'sfunction lookup, including the XSD cast constructors (
xsd:boolean,xsd:integer,xsd:float,xsd:double,xsd:decimal,xsd:string—both compact and full-IRI forms). The only deliberate exception is
in/not-ininside select expressions; the bracketed-list syntax theyrequire can't ride the
SexprTokenchannel.Bugs fixed in the same change
Quoted vs unquoted token collapse.
SexprToken::Atompreviously heldboth
falseand"false", so a select expression argument lost itsliteral type. Split into
Atom(unquoted: variables, numbers, booleans,symbols) and
String(quoted literal). Strict accessors (expect_atom)reject the new
Stringvariant; newas_str()returns inner text fromeither.
(coalesce ?v "false")now correctly yields the string"false".Chained post-aggregate binds. A select expression that referenced
another post-aggregate bind (e.g.
(as (+ ?adjusted 1) ?again), where?adjusteditself was a post-bind) was routed as a pre-aggregationPattern::Bindand evaluated against an unbound variable. Lowering nowtracks post-bind aliases as it walks select columns and routes chained
references to
options.post_bindsin source order.Where the lowering happens
fluree-db-query/src/parse/ast.rs— addedUnresolvedColumn::Expr.fluree-db-query/src/parse/sexpr_tokenize.rs— splitAtom/String.fluree-db-query/src/parse/filter_sexpr.rs—expr_from_sexpr_tokenkeepsliteral type for
Stringtokens.fluree-db-query/src/parse/mod.rs—parse_select_stringdispatchesaggregate vs scalar after a single tokenize; removed dead helpers; added
XSD cast names.
fluree-db-query/src/parse/lower.rs—lower_querydesugars Expr columnsto
Pattern::Bind(oroptions.post_bindsif the expr references anaggregate output or earlier post-bind alias). Mirrors SPARQL's
lower_select_expression_binds.Subqueries support pre-aggregation expression binds; post-aggregation refs
inside subqueries are explicitly rejected because
SubqueryPatternhas nopost_bindsfield (separate enhancement).Tests
fluree-db-queryunit tests:Pattern::Bind(+ ?cnt 1)?cnt→?adjusted→?again) all inpost_binds"false"/"42"/"?notavar"stay string literalsFunction::XsdInteger/Function::XsdStringString("false")andAtom("false")fluree-db-apiintegration tests:Docs
docs/query/jsonld-query.md:selectcovering both aggregateand scalar forms
["count", "?var"]examples that the parser actuallyrejects
stddev, groupconcat with separator)