1
0
mirror of https://github.com/MontFerret/ferret.git synced 2026-06-20 01:17:53 +02:00

chore: update FqlParser to include queryOptionsOpt in grammar definition (#951)

This commit is contained in:
Tim Voronov
2026-06-15 10:41:07 -04:00
committed by GitHub
parent ec32c56ef0
commit 1db7e15906
26 changed files with 2572 additions and 1921 deletions
+5
View File
@@ -19,6 +19,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
@@ -34,9 +35,11 @@ github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQ
github.com/segmentio/encoding v0.3.4 h1:WM4IBnxH8B9TakiM2QD5LyNl9JSndh88QbHqVC+Pauc=
github.com/segmentio/encoding v0.3.4/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM=
github.com/smarty/assertions v1.16.0 h1:EvHNkdRA4QHMrn75NZSoUQ/mAUXAYWfatfB01yTCzfY=
github.com/smarty/assertions v1.16.0/go.mod h1:duaaFdCS0K9dnoM50iyek/eYINOZ64gbh1Xlf6LG7AI=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
@@ -44,5 +47,7 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV
github.com/wI2L/jettison v0.7.4 h1:ptjriu75R/k5RAZO0DJzy2t55f7g+dPiBxBY38icaKg=
github.com/wI2L/jettison v0.7.4/go.mod h1:O+F+T7X7ZN6kTsd167Qk4aZMC8jNrH48SMedNmkfPb0=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+25 -11
View File
@@ -95,16 +95,19 @@ func (c *exprQueryCompiler) emitQueryEnvelope(ctx fql.IQueryExpressionContext, s
queryReg := c.ctx.Function.Registers.Allocate()
c.ctx.Program.Emitter.WithSpan(span, func() {
c.ctx.Program.Emitter.EmitArray(queryReg, 3)
c.ctx.Program.Emitter.EmitArray(queryReg, 4)
})
kind := c.compileQueryKindOperand(ctx)
c.emitQueryEnvelopeOperand(span, queryReg, kind)
payload := c.compileQueryPayloadOperand(ctx.QueryPayload())
c.emitQueryEnvelopeOperand(span, queryReg, payload)
expression := c.compileQueryExpressionOperand(ctx.QueryPayload())
c.emitQueryEnvelopeOperand(span, queryReg, expression)
options := c.compileQueryOptionsOperand(ctx.QueryWithOpt())
params := c.compileQueryParamsOperand(ctx.QueryWithOpt())
c.emitQueryEnvelopeOperand(span, queryReg, params)
options := c.compileQueryOptionsOperand(ctx.QueryOptionsOpt())
c.emitQueryEnvelopeOperand(span, queryReg, options)
return queryReg
@@ -119,7 +122,7 @@ func (c *exprQueryCompiler) compileQueryKindOperand(ctx fql.IQueryExpressionCont
return c.facts.LoadConstant(runtime.NewString(kind))
}
func (c *exprQueryCompiler) compileQueryPayloadOperand(ctx fql.IQueryPayloadContext) bytecode.Operand {
func (c *exprQueryCompiler) compileQueryExpressionOperand(ctx fql.IQueryPayloadContext) bytecode.Operand {
if ctx == nil {
return c.facts.LoadConstant(runtime.EmptyString)
}
@@ -143,7 +146,15 @@ func (c *exprQueryCompiler) compileQueryPayloadOperand(ctx fql.IQueryPayloadCont
return c.facts.LoadConstant(runtime.EmptyString)
}
func (c *exprQueryCompiler) compileQueryOptionsOperand(ctx fql.IQueryWithOptContext) bytecode.Operand {
func (c *exprQueryCompiler) compileQueryParamsOperand(ctx fql.IQueryWithOptContext) bytecode.Operand {
if ctx == nil || ctx.Expression() == nil {
return c.facts.LoadConstant(runtime.None)
}
return c.callbacks.compileExpr(ctx.Expression())
}
func (c *exprQueryCompiler) compileQueryOptionsOperand(ctx fql.IQueryOptionsOptContext) bytecode.Operand {
if ctx == nil || ctx.Expression() == nil {
return c.facts.LoadConstant(runtime.None)
}
@@ -181,7 +192,7 @@ func (c *exprQueryCompiler) compileQueryLiteral(ctx fql.IQueryLiteralContext) by
span := diagnostics.SpanFromRuleContext(ctx)
c.ctx.Program.Emitter.WithSpan(span, func() {
c.ctx.Program.Emitter.EmitArray(dst, 3)
c.ctx.Program.Emitter.EmitArray(dst, 4)
})
kindReg := c.facts.LoadConstant(runtime.NewString(kind))
@@ -190,17 +201,17 @@ func (c *exprQueryCompiler) compileQueryLiteral(ctx fql.IQueryLiteralContext) by
c.ctx.Program.Emitter.EmitArrayPush(dst, kindReg)
})
payloadReg := c.facts.LoadConstant(runtime.EmptyString)
expressionReg := c.facts.LoadConstant(runtime.EmptyString)
if str := ctx.StringLiteral(); str != nil {
if val, ok := parseStringLiteralConst(str); ok {
payloadReg = c.facts.LoadConstant(val)
expressionReg = c.facts.LoadConstant(val)
} else {
payloadReg = c.literals.CompileStringLiteral(str)
expressionReg = c.literals.CompileStringLiteral(str)
}
}
c.ctx.Program.Emitter.WithSpan(span, func() {
c.ctx.Program.Emitter.EmitArrayPush(dst, payloadReg)
c.ctx.Program.Emitter.EmitArrayPush(dst, expressionReg)
})
params := ctx.Expression()
@@ -212,8 +223,11 @@ func (c *exprQueryCompiler) compileQueryLiteral(ctx fql.IQueryLiteralContext) by
paramsReg = c.callbacks.compileExpr(params)
}
optionsReg := c.facts.LoadConstant(runtime.None)
c.ctx.Program.Emitter.WithSpan(span, func() {
c.ctx.Program.Emitter.EmitArrayPush(dst, paramsReg)
c.ctx.Program.Emitter.EmitArrayPush(dst, optionsReg)
})
if dst.IsRegister() {
+25 -14
View File
@@ -569,7 +569,7 @@ func (f *expressionFormatter) formatQueryExpression(ctx *fql.QueryExpressionCont
return
}
if ctx.QueryWithOpt() == nil || f.p.forceSingleLine {
if (ctx.QueryWithOpt() == nil && ctx.QueryOptionsOpt() == nil) || f.p.forceSingleLine {
f.formatQueryExpressionWith(f.p, ctx, true)
return
}
@@ -623,25 +623,36 @@ func (f *expressionFormatter) formatQueryExpressionWith(p *printer, ctx *fql.Que
if with := ctx.QueryWithOpt(); with != nil {
if expr := with.Expression(); expr != nil {
if inline {
p.space()
f.writeKeywordWith(p, keywordWith)
p.space()
f.formatExpressionWith(p, expr.(*fql.ExpressionContext))
} else {
p.newline()
p.withIndent(func() {
f.writeKeywordWith(p, keywordWith)
p.space()
f.formatExpressionWith(p, expr.(*fql.ExpressionContext))
})
}
f.formatQueryClauseWith(p, keywordWith, expr, inline)
}
}
if options := ctx.QueryOptionsOpt(); options != nil {
if expr := options.Expression(); expr != nil {
f.formatQueryClauseWith(p, keywordOptions, expr, inline)
}
}
f.formatRecoveryTailsWith(p, ctx.RecoveryTails())
}
func (f *expressionFormatter) formatQueryClauseWith(p *printer, keyword string, expr fql.IExpressionContext, inline bool) {
if inline {
p.space()
f.writeKeywordWith(p, keyword)
p.space()
f.formatExpressionWith(p, expr.(*fql.ExpressionContext))
return
}
p.newline()
p.withIndent(func() {
f.writeKeywordWith(p, keyword)
p.space()
f.formatExpressionWith(p, expr.(*fql.ExpressionContext))
})
}
func (f *expressionFormatter) writeQueryModifierWith(p *printer, modifier fql.IQueryModifierContext) {
if modifier == nil {
return
+18 -4
View File
@@ -113,6 +113,20 @@ func TestExpressionFormatter_QueryExpressionInline(t *testing.T) {
}
}
func TestExpressionFormatter_QueryExpressionOptionsInline(t *testing.T) {
input := "RETURN QUERY `.items` IN doc USING css OPTIONS { timeout: 5000 }"
program := parseProgram(t, input)
expr := mustFirst[*fql.ExpressionContext](t, program)
var buf bytes.Buffer
e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
e.expression.formatExpression(expr)
if got := buf.String(); got != "QUERY `.items` IN doc USING css OPTIONS { timeout: 5000 }" {
t.Fatalf("unexpected query expression formatting: %q", got)
}
}
func TestExpressionFormatter_QueryExpressionParamPayload(t *testing.T) {
input := "RETURN QUERY @q IN doc USING css"
program := parseProgram(t, input)
@@ -229,7 +243,7 @@ func TestExpressionFormatter_ParenthesizedErrorPolicyTail(t *testing.T) {
}
func TestExpressionFormatter_QueryExpressionErrorPolicyTail(t *testing.T) {
input := "RETURN QUERY `.items` IN doc USING css ON ERROR RETURN NONE"
input := "RETURN QUERY `.items` IN doc USING css OPTIONS { timeout: 5000 } ON ERROR RETURN NONE"
program := parseProgram(t, input)
expr := mustFirst[*fql.ExpressionContext](t, program)
@@ -237,13 +251,13 @@ func TestExpressionFormatter_QueryExpressionErrorPolicyTail(t *testing.T) {
e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
e.expression.formatExpression(expr)
if got := buf.String(); got != "QUERY `.items` IN doc USING css ON ERROR RETURN NONE" {
if got := buf.String(); got != "QUERY `.items` IN doc USING css OPTIONS { timeout: 5000 } ON ERROR RETURN NONE" {
t.Fatalf("unexpected query error policy formatting: %q", got)
}
}
func TestExpressionFormatter_QueryExpressionOneModifierWithMultiline(t *testing.T) {
input := "RETURN QUERY ONE `.items` IN doc USING css WITH { limit: 10, timeout: 5 }"
input := "RETURN QUERY ONE `.items` IN doc USING css WITH { limit: 10, timeout: 5 } OPTIONS { retry: 2, delay: 50 }"
program := parseProgram(t, input)
expr := mustFirst[*fql.ExpressionContext](t, program)
@@ -253,7 +267,7 @@ func TestExpressionFormatter_QueryExpressionOneModifierWithMultiline(t *testing.
e := newEngine(source.NewAnonymous(input), &buf, opts)
e.expression.formatExpression(expr)
if got := buf.String(); got != "QUERY ONE `.items` IN doc USING css\n WITH {\n limit: 10,\n timeout: 5\n }" {
if got := buf.String(); got != "QUERY ONE `.items` IN doc USING css\n WITH {\n limit: 10,\n timeout: 5\n }\n OPTIONS {\n retry: 2,\n delay: 50\n }" {
t.Fatalf("unexpected query expression formatting: %q", got)
}
}
+6 -2
View File
@@ -1102,8 +1102,8 @@ implicitMemberExpressionStart
;
queryExpression
: Query queryModifier queryPayload In expression Using dialect=Identifier queryWithOpt? (recoveryTails)?
| Query {!p.isQueryModifierAhead()}? queryPayload In expression Using dialect=Identifier queryWithOpt? (recoveryTails)?
: Query queryModifier queryPayload In expression Using dialect=Identifier queryWithOpt? queryOptionsOpt? (recoveryTails)?
| Query {!p.isQueryModifierAhead()}? queryPayload In expression Using dialect=Identifier queryWithOpt? queryOptionsOpt? (recoveryTails)?
;
queryModifier
@@ -1122,6 +1122,10 @@ queryWithOpt
: With expression
;
queryOptionsOpt
: Options expression
;
queryLiteral
: Identifier (stringLiteral (OpenParen expression CloseParen)?)?
;
+121 -7
View File
@@ -1,6 +1,7 @@
package diagnostics
import (
"fmt"
"strings"
"github.com/MontFerret/ferret/v2/pkg/diagnostics"
@@ -17,14 +18,46 @@ func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending
return false
}
if !hasPrevToken(offending, "QUERY", 10) && !is(offending, "QUERY") && !hasPrevToken(offending, "USING", 10) {
if with := outOfOrderQueryWithNode(src, offending); with != nil {
span := spanFromTokenSafe(with.Token(), src)
err.Message = "WITH must appear before OPTIONS in QUERY"
err.Hint = "Move the WITH clause before OPTIONS."
err.Spans = []diagnostics.ErrorSpan{
diagnostics.NewMainErrorSpan(span, "out-of-order WITH clause"),
}
return true
}
if !hasPrevToken(offending, "QUERY", 64) && !is(offending, "QUERY") && !hasPrevToken(offending, "USING", 64) && !sourceHasQueryBefore(src, offending) {
return false
}
if prev := offending.Prev(); prev != nil && is(prev, "WITH") {
if is(offending, "WITH") && (hasTokenBefore(offending, "QUERY", "OPTIONS", 64) || sourceHasQueryOptionsBefore(src, offending)) {
span := spanFromTokenSafe(offending.Token(), src)
err.Message = "WITH must appear before OPTIONS in QUERY"
err.Hint = "Move the WITH clause before OPTIONS."
err.Spans = []diagnostics.ErrorSpan{
diagnostics.NewMainErrorSpan(span, "out-of-order WITH clause"),
}
return true
}
if (is(offending, "WITH") || is(offending, "OPTIONS")) && !hasPrevToken(offending, "USING", 2) {
clause := offending.GetText()
span := spanFromTokenSafe(offending.Token(), src)
err.Message = fmt.Sprintf("Expected query %s expression after %s", queryClauseValueName(clause), clause)
err.Hint = queryClauseHint(clause)
err.Spans = []diagnostics.ErrorSpan{
diagnostics.NewMainErrorSpan(span, "missing expression"),
}
return true
}
if prev := offending.Prev(); prev != nil && (is(prev, "WITH") || is(prev, "OPTIONS")) {
span := spanFromTokenSafe(prev.Token(), src)
err.Message = "Expected options expression after WITH"
err.Hint = "Provide an options expression, e.g. WITH { limit: 10 }."
clause := prev.GetText()
err.Message = fmt.Sprintf("Expected query %s expression after %s", queryClauseValueName(clause), clause)
err.Hint = queryClauseHint(clause)
err.Spans = []diagnostics.ErrorSpan{
diagnostics.NewMainErrorSpan(span, "missing expression"),
}
@@ -56,7 +89,7 @@ func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending
return true
}
if (is(offending, "USING") || is(offending, "WITH") || isEOF(offending)) && hasPrevToken(offending, "IN", 2) {
if (is(offending, "USING") || is(offending, "WITH") || is(offending, "OPTIONS") || isEOF(offending)) && hasPrevToken(offending, "IN", 2) {
span := spanFromTokenSafe(offending.Token(), src)
err.Message = "Expected expression after IN"
err.Hint = "Provide a source expression, e.g. QUERY `.items` IN doc USING css."
@@ -106,7 +139,7 @@ func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending
diagnostics.NewMainErrorSpan(span, "invalid dialect"),
}
return true
} else if next := offending.Next(); next == nil || isEOF(next) || is(next, "WITH") {
} else if next := offending.Next(); next == nil || isEOF(next) || is(next, "WITH") || is(next, "OPTIONS") {
span := spanFromTokenSafe(offending.Token(), src)
err.Message = "Expected dialect identifier after USING"
err.Hint = "Provide a dialect identifier, e.g. USING css."
@@ -117,7 +150,7 @@ func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending
}
}
if is(offending, "WITH") || isEOF(offending) {
if is(offending, "WITH") || is(offending, "OPTIONS") || isEOF(offending) {
span := spanFromTokenSafe(offending.Token(), src)
err.Message = "Expected dialect identifier after USING"
err.Hint = "Provide a dialect identifier, e.g. USING css."
@@ -151,6 +184,87 @@ func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending
return false
}
func queryClauseHint(clause string) string {
switch clause {
case "WITH":
return "Provide a params expression, e.g. WITH { params: [1] }."
case "OPTIONS":
return "Provide an options expression, e.g. OPTIONS { timeout: 5000 }."
default:
return "Provide a query clause expression."
}
}
func queryClauseValueName(clause string) string {
switch clause {
case "WITH":
return "params"
case "OPTIONS":
return "options"
default:
return "clause"
}
}
func sourceHasQueryBefore(src *source.Source, offending *TokenNode) bool {
prefix := sourcePrefixBefore(src, offending)
query := strings.LastIndex(prefix, "QUERY")
using := strings.LastIndex(prefix, "USING")
dispatch := strings.LastIndex(prefix, "DISPATCH")
return query >= 0 && using > query && dispatch < query
}
func sourceHasQueryOptionsBefore(src *source.Source, offending *TokenNode) bool {
prefix := sourcePrefixBefore(src, offending)
query := strings.LastIndex(prefix, "QUERY")
options := strings.LastIndex(prefix, "OPTIONS")
return query >= 0 && options > query
}
func sourceHasOutOfOrderQueryWith(src *source.Source) bool {
if src == nil {
return false
}
content := strings.ToUpper(src.Content())
query := strings.LastIndex(content, "QUERY")
options := strings.LastIndex(content, "OPTIONS")
with := strings.LastIndex(content, "WITH")
dispatch := strings.LastIndex(content, "DISPATCH")
return query >= 0 && options > query && with > options && dispatch < query
}
func outOfOrderQueryWithNode(src *source.Source, offending *TokenNode) *TokenNode {
if !sourceHasOutOfOrderQueryWith(src) {
return nil
}
if is(offending, "WITH") {
return offending
}
if next := offending.Next(); is(next, "WITH") {
return next
}
return nil
}
func sourcePrefixBefore(src *source.Source, offending *TokenNode) string {
if src == nil || offending == nil || offending.Token() == nil {
return ""
}
content := []rune(src.Content())
end := offending.Token().GetStart()
if end < 0 || end > len(content) {
end = len(content)
}
return strings.ToUpper(string(content[:end]))
}
func isMissingQueryLiteral(msg string, offending *TokenNode) bool {
if offending == nil {
return false
File diff suppressed because one or more lines are too long
+1982 -1807
View File
File diff suppressed because it is too large Load Diff
@@ -978,6 +978,12 @@ func (s *BaseFqlParserListener) EnterQueryWithOpt(ctx *QueryWithOptContext) {}
// ExitQueryWithOpt is called when production queryWithOpt is exited.
func (s *BaseFqlParserListener) ExitQueryWithOpt(ctx *QueryWithOptContext) {}
// EnterQueryOptionsOpt is called when production queryOptionsOpt is entered.
func (s *BaseFqlParserListener) EnterQueryOptionsOpt(ctx *QueryOptionsOptContext) {}
// ExitQueryOptionsOpt is called when production queryOptionsOpt is exited.
func (s *BaseFqlParserListener) ExitQueryOptionsOpt(ctx *QueryOptionsOptContext) {}
// EnterQueryLiteral is called when production queryLiteral is entered.
func (s *BaseFqlParserListener) EnterQueryLiteral(ctx *QueryLiteralContext) {}
+4
View File
@@ -631,6 +631,10 @@ func (v *BaseFqlParserVisitor) VisitQueryWithOpt(ctx *QueryWithOptContext) inter
return v.VisitChildren(ctx)
}
func (v *BaseFqlParserVisitor) VisitQueryOptionsOpt(ctx *QueryOptionsOptContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseFqlParserVisitor) VisitQueryLiteral(ctx *QueryLiteralContext) interface{} {
return v.VisitChildren(ctx)
}
+6
View File
@@ -475,6 +475,9 @@ type FqlParserListener interface {
// EnterQueryWithOpt is called when entering the queryWithOpt production.
EnterQueryWithOpt(c *QueryWithOptContext)
// EnterQueryOptionsOpt is called when entering the queryOptionsOpt production.
EnterQueryOptionsOpt(c *QueryOptionsOptContext)
// EnterQueryLiteral is called when entering the queryLiteral production.
EnterQueryLiteral(c *QueryLiteralContext)
@@ -979,6 +982,9 @@ type FqlParserListener interface {
// ExitQueryWithOpt is called when exiting the queryWithOpt production.
ExitQueryWithOpt(c *QueryWithOptContext)
// ExitQueryOptionsOpt is called when exiting the queryOptionsOpt production.
ExitQueryOptionsOpt(c *QueryOptionsOptContext)
// ExitQueryLiteral is called when exiting the queryLiteral production.
ExitQueryLiteral(c *QueryLiteralContext)
+3
View File
@@ -475,6 +475,9 @@ type FqlParserVisitor interface {
// Visit a parse tree produced by FqlParser#queryWithOpt.
VisitQueryWithOpt(ctx *QueryWithOptContext) interface{}
// Visit a parse tree produced by FqlParser#queryOptionsOpt.
VisitQueryOptionsOpt(ctx *QueryOptionsOptContext) interface{}
// Visit a parse tree produced by FqlParser#queryLiteral.
VisitQueryLiteral(ctx *QueryLiteralContext) interface{}
+7 -4
View File
@@ -5,11 +5,14 @@ import (
)
type (
// Query represents a query literal used by the operator index.
// Query describes a query operation passed to a Queryable value.
// Params contains query input supplied by WITH, while Options contains
// execution policy supplied by OPTIONS.
Query struct {
Options Value `json:"options"`
Kind String `json:"kind"`
Payload String `json:"payload"` // TODO: Rename to "expression" or "value"
Options Value `json:"options"`
Params Value `json:"params"`
Kind String `json:"kind"`
Expression String `json:"expression"`
}
// QueryFunc is the list-returning query implementation used by default modifier helpers.
+30
View File
@@ -2,6 +2,7 @@ package runtime
import (
"context"
"encoding/json"
"testing"
)
@@ -47,3 +48,32 @@ func TestDefaultQueryOneReturnsFirstOrNone(t *testing.T) {
})
}
}
func TestQueryJSONFieldNames(t *testing.T) {
data, err := json.Marshal(Query{
Options: None,
Params: NewObjectWith(map[string]Value{"value": NewInt(1)}),
Kind: NewString("capture"),
Expression: NewString("test"),
})
if err != nil {
t.Fatalf("failed to marshal query: %v", err)
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
t.Fatalf("failed to unmarshal query fields: %v", err)
}
for _, key := range []string{"options", "params", "kind", "expression"} {
if _, ok := fields[key]; !ok {
t.Fatalf("expected query JSON field %q in %s", key, data)
}
}
if len(fields) != 4 {
t.Fatalf("expected exactly four query JSON fields, got %d in %s", len(fields), data)
}
if _, ok := fields["payload"]; ok {
t.Fatalf("did not expect legacy payload field in %s", data)
}
}
+59 -14
View File
@@ -98,12 +98,40 @@ func coerceDispatchArgs(
func coerceQueryDescriptor(ctx context.Context, descriptor runtime.Value) (runtime.Query, error) {
switch value := descriptor.(type) {
case runtime.ObjectLike:
length, err := value.Length(ctx)
if err != nil {
return runtime.Query{}, err
}
if length != 4 {
return runtime.Query{}, runtime.Error(runtime.ErrInvalidOperation, errQueryFormatUnexpected)
}
for _, key := range []runtime.Value{
queryDescriptorKeyKind,
queryDescriptorKeyExpression,
queryDescriptorKeyParams,
queryDescriptorKeyOptions,
} {
ok, err := value.ContainsKey(ctx, key)
if err != nil {
return runtime.Query{}, err
}
if !ok {
return runtime.Query{}, runtime.Error(runtime.ErrInvalidOperation, errQueryFormatUnexpected)
}
}
kind, err := value.Get(ctx, queryDescriptorKeyKind)
if err != nil {
return runtime.Query{}, err
}
payload, err := value.Get(ctx, queryDescriptorKeyPayload)
expression, err := value.Get(ctx, queryDescriptorKeyExpression)
if err != nil {
return runtime.Query{}, err
}
params, err := value.Get(ctx, queryDescriptorKeyParams)
if err != nil {
return runtime.Query{}, err
}
@@ -113,10 +141,21 @@ func coerceQueryDescriptor(ctx context.Context, descriptor runtime.Value) (runti
return runtime.Query{}, err
}
kindString, err := runtime.CastString(kind)
if err != nil {
return runtime.Query{}, runtime.TypeErrorOf(kind, runtime.TypeString)
}
expressionString, err := runtime.CastString(expression)
if err != nil {
return runtime.Query{}, runtime.TypeErrorOf(expression, runtime.TypeString)
}
return runtime.Query{
Kind: runtime.CastOr[runtime.String](kind, runtime.EmptyString),
Payload: runtime.CastOr[runtime.String](payload, runtime.EmptyString),
Options: options,
Kind: kindString,
Expression: expressionString,
Params: params,
Options: options,
}, nil
case *runtime.Array:
length, err := value.Length(ctx)
@@ -124,7 +163,7 @@ func coerceQueryDescriptor(ctx context.Context, descriptor runtime.Value) (runti
return runtime.Query{}, err
}
if length != 3 {
if length != 4 {
return runtime.Query{}, runtime.Error(runtime.ErrInvalidOperation, errQueryFormatUnexpected)
}
@@ -133,12 +172,17 @@ func coerceQueryDescriptor(ctx context.Context, descriptor runtime.Value) (runti
return runtime.Query{}, err
}
payloadVal, err := value.At(ctx, runtime.NewInt(1))
expressionVal, err := value.At(ctx, runtime.NewInt(1))
if err != nil {
return runtime.Query{}, err
}
optionsVal, err := value.At(ctx, runtime.NewInt(2))
paramsVal, err := value.At(ctx, runtime.NewInt(2))
if err != nil {
return runtime.Query{}, err
}
optionsVal, err := value.At(ctx, runtime.NewInt(3))
if err != nil {
return runtime.Query{}, err
}
@@ -148,18 +192,19 @@ func coerceQueryDescriptor(ctx context.Context, descriptor runtime.Value) (runti
return runtime.Query{}, runtime.TypeErrorOf(kindVal, runtime.TypeString)
}
payload := runtime.EmptyString
if payloadVal != runtime.None {
payload, err = runtime.CastString(payloadVal)
expression := runtime.EmptyString
if expressionVal != runtime.None {
expression, err = runtime.CastString(expressionVal)
if err != nil {
return runtime.Query{}, runtime.TypeErrorOf(payloadVal, runtime.TypeString, runtime.TypeNone)
return runtime.Query{}, runtime.TypeErrorOf(expressionVal, runtime.TypeString, runtime.TypeNone)
}
}
return runtime.Query{
Kind: kind,
Payload: payload,
Options: optionsVal,
Kind: kind,
Expression: expression,
Params: paramsVal,
Options: optionsVal,
}, nil
default:
return runtime.Query{}, runtime.Error(runtime.ErrInvalidOperation, errQueryFormatUnexpected)
+8 -6
View File
@@ -7,17 +7,19 @@ import (
)
const (
queryDescriptorKind = "kind"
queryDescriptorPayload = "payload"
queryDescriptorOptions = "options"
queryDescriptorKind = "kind"
queryDescriptorExpression = "expression"
queryDescriptorParams = "params"
queryDescriptorOptions = "options"
errQueryFormatUnexpected = "unexpected query format"
)
var (
queryDescriptorKeyKind = runtime.NewString(queryDescriptorKind)
queryDescriptorKeyPayload = runtime.NewString(queryDescriptorPayload)
queryDescriptorKeyOptions = runtime.NewString(queryDescriptorOptions)
queryDescriptorKeyKind = runtime.NewString(queryDescriptorKind)
queryDescriptorKeyExpression = runtime.NewString(queryDescriptorExpression)
queryDescriptorKeyParams = runtime.NewString(queryDescriptorParams)
queryDescriptorKeyOptions = runtime.NewString(queryDescriptorOptions)
)
func applyQuery(ctx context.Context, src runtime.Value, descriptor runtime.Value) (runtime.Value, error) {
+62 -16
View File
@@ -99,6 +99,7 @@ func validDescriptor() runtime.Value {
runtime.NewString("css"),
runtime.NewString(".items"),
runtime.None,
runtime.None,
)
}
@@ -131,7 +132,7 @@ func assertStringArray(t *testing.T, out runtime.Value, expected ...runtime.Stri
}
}
func TestApplyQuery_ObjectDescriptorIgnoresModifier(t *testing.T) {
func TestApplyQuery_ObjectDescriptorUsesCanonicalShape(t *testing.T) {
src := &queryStub{
result: runtime.NewArrayWith(runtime.NewString("ok")),
}
@@ -139,9 +140,9 @@ func TestApplyQuery_ObjectDescriptorIgnoresModifier(t *testing.T) {
obj := runtime.NewObject()
ctx := context.Background()
_ = obj.Set(ctx, runtime.NewString("kind"), runtime.NewString("css"))
_ = obj.Set(ctx, runtime.NewString("payload"), runtime.NewString(".items"))
_ = obj.Set(ctx, runtime.NewString("expression"), runtime.NewString(".items"))
_ = obj.Set(ctx, runtime.NewString("params"), runtime.NewString("input"))
_ = obj.Set(ctx, runtime.NewString("options"), runtime.None)
_ = obj.Set(ctx, runtime.NewString("modifier"), runtime.NewString("ONE"))
out, err := applyQuery(ctx, src, obj)
if err != nil {
@@ -153,21 +154,21 @@ func TestApplyQuery_ObjectDescriptorIgnoresModifier(t *testing.T) {
if len(src.queries) != 1 {
t.Fatalf("unexpected query count: got %d, want 1", len(src.queries))
}
if src.queries[0].Params != runtime.NewString("input") || src.queries[0].Options != runtime.None {
t.Fatalf("unexpected query params/options: %#v", src.queries[0])
}
}
func TestApplyQuery_ArrayDescriptorRequiresExactTupleSize(t *testing.T) {
src := &queryStub{
result: runtime.NewArrayWith(runtime.NewString("ok")),
}
func TestApplyQuery_ObjectDescriptorRejectsExtraFields(t *testing.T) {
obj := runtime.NewObjectWith(map[string]runtime.Value{
"kind": runtime.NewString("css"),
"expression": runtime.NewString(".items"),
"params": runtime.None,
"options": runtime.None,
"modifier": runtime.NewString("ONE"),
})
descriptor := runtime.NewArrayWith(
runtime.NewString("css"),
runtime.NewString(".items"),
runtime.None,
runtime.NewString("count"),
)
_, err := applyQuery(context.Background(), src, descriptor)
_, err := applyQuery(context.Background(), &queryStub{}, obj)
if err == nil {
t.Fatal("expected runtime error")
}
@@ -177,7 +178,51 @@ func TestApplyQuery_ArrayDescriptorRequiresExactTupleSize(t *testing.T) {
}
}
func TestApplyQuery_ArrayDescriptorPayloadTypeValidation(t *testing.T) {
func TestApplyQuery_ObjectDescriptorRejectsLegacyPayloadShape(t *testing.T) {
obj := runtime.NewObjectWith(map[string]runtime.Value{
"kind": runtime.NewString("css"),
"payload": runtime.NewString(".items"),
"params": runtime.None,
"options": runtime.None,
})
_, err := applyQuery(context.Background(), &queryStub{}, obj)
if err == nil {
t.Fatal("expected runtime error")
}
if !strings.Contains(strings.ToLower(err.Error()), "unexpected query format") {
t.Fatalf("expected unexpected query format error, got %v", err)
}
}
func TestApplyQuery_ArrayDescriptorRequiresExactTupleSize(t *testing.T) {
for _, descriptor := range []*runtime.Array{
runtime.NewArrayWith(
runtime.NewString("css"),
runtime.NewString(".items"),
runtime.None,
),
runtime.NewArrayWith(
runtime.NewString("css"),
runtime.NewString(".items"),
runtime.None,
runtime.None,
runtime.None,
),
} {
_, err := applyQuery(context.Background(), &queryStub{}, descriptor)
if err == nil {
t.Fatal("expected runtime error")
}
if !strings.Contains(strings.ToLower(err.Error()), "unexpected query format") {
t.Fatalf("expected unexpected query format error, got %v", err)
}
}
}
func TestApplyQuery_ArrayDescriptorExpressionTypeValidation(t *testing.T) {
src := &queryStub{
result: runtime.NewArrayWith(runtime.NewString("ok")),
}
@@ -186,6 +231,7 @@ func TestApplyQuery_ArrayDescriptorPayloadTypeValidation(t *testing.T) {
runtime.NewString("css"),
runtime.NewInt(1),
runtime.None,
runtime.None,
)
_, err := applyQuery(context.Background(), src, descriptor)
+13
View File
@@ -13,6 +13,11 @@ RETURN [
QUERY ONE ".items" IN @doc USING css,
]`
const queryableConfiguredQuery = `
RETURN QUERY ".items" IN @doc USING css
WITH { value: 1 }
OPTIONS { timeout: 5000 }`
func BenchmarkQueryableModifiers_O0(b *testing.B) {
RunBenchmarkO0(b, queryableModifiersQuery, vm.WithParam("doc", newBenchmarkQueryable()))
}
@@ -20,3 +25,11 @@ func BenchmarkQueryableModifiers_O0(b *testing.B) {
func BenchmarkQueryableModifiers_O1(b *testing.B) {
RunBenchmarkO1(b, queryableModifiersQuery, vm.WithParam("doc", newBenchmarkQueryable()))
}
func BenchmarkQueryableConfigured_O0(b *testing.B) {
RunBenchmarkO0(b, queryableConfiguredQuery, vm.WithParam("doc", newBenchmarkQueryable()))
}
func BenchmarkQueryableConfigured_O1(b *testing.B) {
RunBenchmarkO1(b, queryableConfiguredQuery, vm.WithParam("doc", newBenchmarkQueryable()))
}
@@ -3,7 +3,9 @@ package compiler_test
import (
"testing"
"github.com/MontFerret/ferret/v2/pkg/compiler"
parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
"github.com/MontFerret/ferret/v2/pkg/source"
"github.com/MontFerret/ferret/v2/test/spec"
. "github.com/MontFerret/ferret/v2/test/spec/compile"
)
@@ -113,10 +115,47 @@ func TestSyntaxErrorsQueryExpression(t *testing.T) {
"RETURN QUERY `.x` IN @doc USING css WITH )",
E{
Kind: parserd.SyntaxError,
Message: "Expected options expression after WITH",
Hint: "Provide an options expression, e.g. WITH { limit: 10 }.",
Message: "Expected query params expression after WITH",
Hint: "Provide a params expression, e.g. WITH { params: [1] }.",
},
"Missing WITH value",
).Skip(),
),
Failure(
"RETURN QUERY `.x` IN @doc USING css OPTIONS )",
E{
Kind: parserd.SyntaxError,
Message: "Expected query options expression after OPTIONS",
Hint: "Provide an options expression, e.g. OPTIONS { timeout: 5000 }.",
},
"Missing OPTIONS value",
),
Failure(
"RETURN QUERY `.x` IN @doc USING css OPTIONS { timeout: 5000 } WITH { params: [1] }",
E{
Kind: parserd.SyntaxError,
Message: "WITH must appear before OPTIONS in QUERY",
Hint: "Move the WITH clause before OPTIONS.",
},
"WITH after OPTIONS",
),
})
}
func TestSyntaxErrorsQueryExpressionWithAfterOptionsSpan(t *testing.T) {
query := "RETURN QUERY `.x` IN @doc USING css OPTIONS { timeout: 5000 } WITH { params: [1] }"
_, err := compiler.New().Compile(source.NewAnonymous(query))
if err == nil {
t.Fatal("expected compilation error")
}
diag := firstCompilationError(err)
if diag == nil || len(diag.Spans) == 0 {
t.Fatalf("expected diagnostic span, got %v", err)
}
span := diag.Spans[0].Span
if got := query[span.Start:span.End]; got != "WITH" {
t.Fatalf("expected diagnostic to point at WITH, got %q", got)
}
}
@@ -10,7 +10,7 @@ import (
"github.com/MontFerret/ferret/v2/test/spec/compile/inspect"
)
func threeSlotQueryDescriptorFor(code []bytecode.Instruction, opcode bytecode.Opcode) error {
func fourSlotQueryDescriptorFor(code []bytecode.Instruction, opcode bytecode.Opcode) error {
applyIdx, ok := inspect.FindFirstOpcodeIndex(code, opcode)
if !ok {
return fmt.Errorf("expected %s in bytecode", opcode)
@@ -21,15 +21,15 @@ func threeSlotQueryDescriptorFor(code []bytecode.Instruction, opcode bytecode.Op
return fmt.Errorf("expected OpLoadArray for query descriptor before OpQuery")
}
if size != 3 {
return fmt.Errorf("expected 3-slot query descriptor, got %d", size)
if size != 4 {
return fmt.Errorf("expected 4-slot query descriptor, got %d", size)
}
return nil
}
func threeSlotQueryDescriptor(code []bytecode.Instruction) error {
return threeSlotQueryDescriptorFor(code, bytecode.OpQuery)
func fourSlotQueryDescriptor(code []bytecode.Instruction) error {
return fourSlotQueryDescriptorFor(code, bytecode.OpQuery)
}
func TestQueryExpressionSourceImplicitCurrentCompiles(t *testing.T) {
@@ -38,7 +38,7 @@ func TestQueryExpressionSourceImplicitCurrentCompiles(t *testing.T) {
LET sections = @sections
LET linksBySection = sections[* RETURN (QUERY "a" IN . USING css)]
RETURN linksBySection[**]`, func(prog *bytecode.Program) error {
return threeSlotQueryDescriptor(prog.Bytecode)
return fourSlotQueryDescriptor(prog.Bytecode)
}, "Should compile query expression with implicit current source"),
})
}
@@ -46,7 +46,7 @@ RETURN linksBySection[**]`, func(prog *bytecode.Program) error {
func TestQueryModifierLowering_OneUsesDirectOpcode(t *testing.T) {
RunSpecs(t, []spec.Spec{
ProgramCheck(`RETURN QUERY ONE ".items" IN @doc USING css`, func(prog *bytecode.Program) error {
if err := threeSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQueryOne); err != nil {
if err := fourSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQueryOne); err != nil {
return err
}
if inspect.HasOpcode(prog, bytecode.OpLength) {
@@ -70,7 +70,7 @@ func TestQueryModifierLowering_OneUsesDirectOpcode(t *testing.T) {
func TestQueryShorthandLowering(t *testing.T) {
RunSpecs(t, []spec.Spec{
ProgramCheck("RETURN @doc[~ css`.items`]", func(prog *bytecode.Program) error {
if err := threeSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQuery); err != nil {
if err := fourSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQuery); err != nil {
return err
}
if inspect.HasOpcode(prog, bytecode.OpQueryOne) {
@@ -80,7 +80,7 @@ func TestQueryShorthandLowering(t *testing.T) {
return nil
}, "regular query shorthand lowering"),
ProgramCheck("RETURN @doc[~? css`.items`]", func(prog *bytecode.Program) error {
if err := threeSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQueryOne); err != nil {
if err := fourSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQueryOne); err != nil {
return err
}
if inspect.HasOpcode(prog, bytecode.OpQuery) {
@@ -115,7 +115,7 @@ func TestQueryModifierLowering_ExistsCount(t *testing.T) {
specs := make([]spec.Spec, 0, len(cases))
for _, tc := range cases {
specs = append(specs, ProgramCheck(tc.expr, func(prog *bytecode.Program) error {
if err := threeSlotQueryDescriptorFor(prog.Bytecode, tc.opcode); err != nil {
if err := fourSlotQueryDescriptorFor(prog.Bytecode, tc.opcode); err != nil {
return err
}
@@ -74,6 +74,12 @@ FOR u IN users
Env: []vm.EnvironmentOption{vm.WithParams(runtime.Params{"doc": queryable})},
Description: "Query literal with params",
},
{
Input: spec.NewExpressionInput("RETURN QUERY `.items` IN @doc USING css WITH { value: 1 } OPTIONS { timeout: 5000 }"),
Expected: []any{"ok"},
Env: []vm.EnvironmentOption{vm.WithParams(runtime.Params{"doc": queryable})},
Description: "Configured query expression",
},
{
Input: spec.NewExpressionInput(`
FUNC add(x, y) => x + y
@@ -98,6 +98,12 @@ FOR u IN users
Env: []vm.EnvironmentOption{vm.WithParams(map[string]runtime.Value{"doc": queryable})},
Description: "Query literal without params",
},
{
Input: spec.NewExpressionInput("RETURN QUERY `.items` IN @doc USING css WITH { value: 1 } OPTIONS { timeout: 5000 }"),
Expected: []any{"ok"},
Env: []vm.EnvironmentOption{vm.WithParams(map[string]runtime.Value{"doc": queryable})},
Description: "Configured query expression",
},
{
Input: spec.NewExpressionInput(`
FUNC add(x, y) => x + y
+3 -2
View File
@@ -73,6 +73,7 @@ func programWithApplyQueryConstSource(src runtime.Value) *bytecode.Program {
runtime.NewString("css"),
runtime.NewString(".items"),
runtime.None,
runtime.None,
),
},
}
@@ -98,8 +99,8 @@ func TestApplyQueryConstantSource_Strict(t *testing.T) {
return fmt.Errorf("unexpected query kind: got %q, want %q", got, want)
}
if got, want := stub.queries[0].Payload, runtime.NewString(".items"); got != want {
return fmt.Errorf("unexpected query payload: got %q, want %q", got, want)
if got, want := stub.queries[0].Expression, runtime.NewString(".items"); got != want {
return fmt.Errorf("unexpected query expression: got %q, want %q", got, want)
}
return nil
+121 -18
View File
@@ -3,6 +3,7 @@ package vm_test
import (
"context"
"fmt"
"strings"
"testing"
"github.com/MontFerret/ferret/v2/pkg/runtime"
@@ -21,12 +22,16 @@ func TestQueryable(t *testing.T) {
Array("RETURN @doc[~ css`.items`]", []any{"ok"}, "Should apply query literal"),
S("RETURN @doc[~ css`.items`][0]", "ok", "Should apply query literal and index tail"),
Array("RETURN QUERY `.items` IN @doc USING css", []any{"ok"}, "Should apply query expression"),
Array("RETURN QUERY @q IN @doc USING css", []any{"ok"}, "Should apply query expression with param payload"),
Array("LET q = \".dynamic-var\"\nRETURN QUERY q IN @doc USING css", []any{"ok"}, "Should apply query expression with variable payload"),
Array("RETURN QUERY @q IN @doc USING css", []any{"ok"}, "Should apply query expression with param expression"),
Array("LET q = \".dynamic-var\"\nRETURN QUERY q IN @doc USING css", []any{"ok"}, "Should apply query expression with variable expression"),
Array("RETURN QUERY `.with` IN @doc USING css WITH { value: 1 }", []any{"ok"}, "Should apply query expression with params"),
Array("RETURN QUERY `.options` IN @doc USING css OPTIONS { timeout: 5000 }", []any{"ok"}, "Should apply query expression with options"),
Array("RETURN QUERY `.both` IN @doc USING css WITH { value: 2 } OPTIONS { timeout: 6000 }", []any{"ok"}, "Should apply query expression with params and options"),
S("RETURN QUERY ONE `.one-both` IN @doc USING css WITH { value: 3 } OPTIONS { timeout: 7000 }", "ok", "Should apply query-one expression with params and options"),
Array("RETURN @doc[~ sql`SELECT * FROM products`({ c: \"laptops\" })]", []any{"ok"}, "Should apply query literal with params"),
S("RETURN @doc[~? sql`SELECT * FROM featured`({ c: \"tablets\" })]", "ok", "Should apply query-one literal with params"),
Array("RETURN QUERY `SELECT * FROM products` IN @doc USING sql WITH { c: \"phones\" }", []any{"ok"}, "Should apply query expression with options"),
Array("RETURN @doc[~ text]", []any{"ok"}, "Should apply query literal with no string payload"),
Array("RETURN @doc[~ text]", []any{"ok"}, "Should apply query literal with no string expression"),
Array(`RETURN @sections[* RETURN (QUERY "a" IN . USING css)][**]`, []any{"one", "two"}, "Should apply query expression to implicit current source"),
spec.NewSpec("RETURN @val[~ css`x`]").Expect().ExecError(ShouldBeRuntimeError, &ExpectedRuntimeError{
Message: "invalid type",
@@ -46,26 +51,46 @@ func TestQueryable(t *testing.T) {
var hasSQLParamsOne bool
var hasSQLQueryExpr bool
var hasText bool
var hasNoClauses bool
var hasWithOnly bool
var hasOptionsOnly bool
var hasBoth bool
var hasOneBoth bool
for _, q := range queryable.MockQueries() {
switch q.Kind {
case runtime.NewString("css"):
if q.Payload == runtime.NewString(".items") {
if q.Expression == runtime.NewString(".items") {
hasCSS = true
hasNoClauses = q.Params == runtime.None && q.Options == runtime.None
}
if q.Payload == runtime.NewString(".dynamic-param") {
if q.Expression == runtime.NewString(".dynamic-param") {
hasCSSParam = true
}
if q.Payload == runtime.NewString(".dynamic-var") {
if q.Expression == runtime.NewString(".dynamic-var") {
hasCSSVar = true
}
if q.Expression == runtime.NewString(".with") {
hasWithOnly = queryMapValue(t, q.Params, "value") == runtime.NewInt(1) && q.Options == runtime.None
}
if q.Expression == runtime.NewString(".options") {
hasOptionsOnly = q.Params == runtime.None && queryMapValue(t, q.Options, "timeout") == runtime.NewInt(5000)
}
if q.Expression == runtime.NewString(".both") {
hasBoth = queryMapValue(t, q.Params, "value") == runtime.NewInt(2) &&
queryMapValue(t, q.Options, "timeout") == runtime.NewInt(6000)
}
if q.Expression == runtime.NewString(".one-both") {
hasOneBoth = queryMapValue(t, q.Params, "value") == runtime.NewInt(3) &&
queryMapValue(t, q.Options, "timeout") == runtime.NewInt(7000)
}
case runtime.NewString("text"):
if q.Payload == runtime.EmptyString {
if q.Expression == runtime.EmptyString {
hasText = true
}
case runtime.NewString("sql"):
if q.Payload == runtime.NewString("SELECT * FROM products") {
params, err := runtime.ToMap(context.Background(), q.Options)
if q.Expression == runtime.NewString("SELECT * FROM products") {
params, err := runtime.ToMap(context.Background(), q.Params)
if err != nil {
t.Fatalf("sql query expression params decode failed: %v", err)
}
@@ -75,41 +100,56 @@ func TestQueryable(t *testing.T) {
hasSQLQueryExpr = true
}
}
if q.Payload == runtime.NewString("SELECT * FROM featured") {
params, err := runtime.ToMap(context.Background(), q.Options)
if q.Expression == runtime.NewString("SELECT * FROM featured") {
params, err := runtime.ToMap(context.Background(), q.Params)
if err != nil {
t.Fatalf("sql query-one params decode failed: %v", err)
}
value, err := params.Get(context.Background(), runtime.NewString("c"))
if err == nil && value == runtime.NewString("tablets") {
if err == nil && value == runtime.NewString("tablets") && q.Options == runtime.None {
hasSQLParamsOne = true
}
}
params, err := runtime.ToMap(context.Background(), q.Options)
params, err := runtime.ToMap(context.Background(), q.Params)
if err != nil {
t.Fatalf("sql params decode failed: %v", err)
}
value, err := params.Get(context.Background(), runtime.NewString("c"))
if err == nil && value == runtime.NewString("laptops") {
if err == nil && value == runtime.NewString("laptops") && q.Options == runtime.None {
hasSQLParams = true
}
}
}
if !hasCSS {
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and payload %q", "css", ".items"))
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and expression %q", "css", ".items"))
}
if !hasCSSParam {
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and payload %q", "css", ".dynamic-param"))
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and expression %q", "css", ".dynamic-param"))
}
if !hasCSSVar {
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and payload %q", "css", ".dynamic-var"))
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and expression %q", "css", ".dynamic-var"))
}
if !hasText {
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and empty payload", "text"))
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and empty expression", "text"))
}
if !hasNoClauses {
t.Fatal("expected omitted WITH and OPTIONS clauses to produce NONE")
}
if !hasWithOnly {
t.Fatal("expected WITH to populate params without options")
}
if !hasOptionsOnly {
t.Fatal("expected OPTIONS to populate options without params")
}
if !hasBoth {
t.Fatal("expected WITH and OPTIONS to remain distinct")
}
if !hasOneBoth {
t.Fatal("expected QUERY ONE to receive distinct params and options")
}
if !hasSQLQueryExpr {
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and params containing %q=%q", "sql", "c", "phones"))
@@ -123,6 +163,69 @@ func TestQueryable(t *testing.T) {
})
}
func TestQueryExpressionClausesEvaluatedOnce(t *testing.T) {
queryable := mock.NewQueryable(runtime.NewArrayWith(runtime.NewString("ok")))
paramsCalls := 0
optionsCalls := 0
var clauseOrder []string
RunSpecs(t, []spec.Spec{
Array(
"RETURN QUERY `.once` IN @doc USING css WITH PARAMS_FN() OPTIONS OPTIONS_FN()",
[]any{"ok"},
"Should evaluate query params and options once",
),
},
vm.WithParam("doc", queryable),
vm.WithFunction("PARAMS_FN", func(context.Context, ...runtime.Value) (runtime.Value, error) {
paramsCalls++
clauseOrder = append(clauseOrder, "params")
return runtime.NewObjectWith(map[string]runtime.Value{"value": runtime.NewInt(1)}), nil
}),
vm.WithFunction("OPTIONS_FN", func(context.Context, ...runtime.Value) (runtime.Value, error) {
optionsCalls++
clauseOrder = append(clauseOrder, "options")
return runtime.NewObjectWith(map[string]runtime.Value{"timeout": runtime.NewInt(5000)}), nil
}),
)
if paramsCalls != 2 || optionsCalls != 2 {
t.Fatalf("expected params and options to be evaluated once per O0/O1 execution, got params=%d options=%d", paramsCalls, optionsCalls)
}
if got, want := strings.Join(clauseOrder, ","), "params,options,params,options"; got != want {
t.Fatalf("unexpected clause evaluation order: got %q, want %q", got, want)
}
queries := queryable.MockQueries()
if len(queries) != 2 {
t.Fatalf("expected one captured query per O0/O1 execution, got %d", len(queries))
}
for _, query := range queries {
if queryMapValue(t, query.Params, "value") != runtime.NewInt(1) {
t.Fatal("expected captured params")
}
if queryMapValue(t, query.Options, "timeout") != runtime.NewInt(5000) {
t.Fatal("expected captured options")
}
}
}
func queryMapValue(t *testing.T, value runtime.Value, key string) runtime.Value {
t.Helper()
out, err := runtime.ToMap(context.Background(), value)
if err != nil {
t.Fatalf("failed to decode query map: %v", err)
}
item, err := out.Get(context.Background(), runtime.NewString(key))
if err != nil {
t.Fatalf("failed to read query map key %q: %v", key, err)
}
return item
}
func TestComplexQueries(t *testing.T) {
queryableDoc := mock.NewNode("doc")
queryableDB := mock.NewDBQueryable()
+1 -1
View File
@@ -42,7 +42,7 @@ func (n *Node) Iterate(ctx context.Context) (runtime.Iterator, error) {
func (n *Node) Query(_ context.Context, q runtime.Query) (runtime.List, error) {
switch q.Kind.String() {
case "css":
switch q.Payload.String() {
switch q.Expression.String() {
case ".product":
return runtime.NewArrayWith(NewNode("product")), nil
case ".title":
+1 -1
View File
@@ -25,7 +25,7 @@ func (m *DBQueryable) Query(ctx context.Context, q runtime.Query) (runtime.List,
return runtime.NewArray(0), nil
}
params, err := runtime.ToMap(ctx, q.Options)
params, err := runtime.ToMap(ctx, q.Params)
if err != nil {
return nil, err
}